mirror of
https://github.com/rowtype-yoga/ry-blocks.git
synced 2024-09-17 16:27:33 +03:00
Add popper
This commit is contained in:
parent
93cb3b5fd5
commit
774db186ef
@ -15,7 +15,6 @@
|
||||
"@testing-library/react": "^11.0.4",
|
||||
"@testing-library/user-event": "^12.1.7",
|
||||
"babel-loader": "^8.1.0",
|
||||
"source-map": "GerHobbelt/source-map#patch-8",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"compression-webpack-plugin": "^6.0.3",
|
||||
"framer-motion": "^2.7.0",
|
||||
@ -25,6 +24,7 @@
|
||||
"purescript": "^0.13.8",
|
||||
"react-refresh": "^0.8.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map": "GerHobbelt/source-map#patch-8",
|
||||
"source-map-loader": "^1.1.2",
|
||||
"spago": "^0.16.0",
|
||||
"svg2psreact": "^2.1.0",
|
||||
@ -50,8 +50,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/core": "^10.0.28",
|
||||
"@popperjs/core": "^2.6.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-popper": "^2.2.4",
|
||||
"react-syntax-highlighter": "^15.3.1"
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ zip (TwoOrMore as) (TwoOrMore bs) =
|
||||
}
|
||||
|
||||
findIndex ∷ ∀ a. (a -> Boolean) -> TwoOrMore a -> Maybe Int
|
||||
findIndex f (TwoOrMore { first, second, rest }) =
|
||||
findIndex f (TwoOrMore { first, second, rest }) = do
|
||||
if f first then
|
||||
Just 0
|
||||
else
|
||||
|
@ -1,6 +1,7 @@
|
||||
const framerMotion = require("framer-motion")
|
||||
|
||||
exports.divImpl = framerMotion.motion.div
|
||||
exports.buttonImpl = framerMotion.motion.button
|
||||
exports.h1Impl = framerMotion.motion.h1
|
||||
exports.svgImpl = framerMotion.motion.svg
|
||||
exports.pathImpl = framerMotion.motion.path
|
||||
|
@ -1,6 +1,21 @@
|
||||
module Framer.Motion
|
||||
( animate
|
||||
, makeVariantLabels
|
||||
, WhileTap
|
||||
, whileTap
|
||||
, callback
|
||||
, class EffectFnMaker
|
||||
, toEffectFn
|
||||
, OnTap
|
||||
, TapInfo
|
||||
, OnTapStart
|
||||
, OnTapEnd
|
||||
, onTapStart
|
||||
, onTapEnd
|
||||
, onTapCancel
|
||||
, onTap
|
||||
, OnTapCancel
|
||||
, TargetAndTransition
|
||||
, MakeVariantLabel
|
||||
, div
|
||||
, h1
|
||||
@ -14,6 +29,7 @@ module Framer.Motion
|
||||
, prop
|
||||
, Drag
|
||||
, DragMomentum
|
||||
, button
|
||||
, dragMomentum
|
||||
, DragPropagation
|
||||
, DragElastic
|
||||
@ -63,13 +79,13 @@ import Prelude
|
||||
import Data.Nullable (Nullable)
|
||||
import Data.Symbol (class IsSymbol, SProxy, reflectSymbol)
|
||||
import Effect (Effect)
|
||||
import Effect.Uncurried (EffectFn2, mkEffectFn2)
|
||||
import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2)
|
||||
import Foreign.Object (Object)
|
||||
import Heterogeneous.Mapping (class HMapWithIndex, class MappingWithIndex, hmapWithIndex)
|
||||
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, css)
|
||||
import React.Basic.DOM (CSS, Props_div, Props_h1, Props_button, css)
|
||||
import React.Basic.DOM.Internal (SharedSVGProps)
|
||||
import React.Basic.DOM.SVG (Props_svg, Props_rect, Props_path)
|
||||
import React.Basic.Events (EventHandler)
|
||||
@ -85,6 +101,8 @@ import Yoga.Block.Internal (Id)
|
||||
|
||||
foreign import divImpl ∷ ∀ a. ReactComponent { | a }
|
||||
|
||||
foreign import buttonImpl ∷ ∀ a. ReactComponent { | a }
|
||||
|
||||
foreign import h1Impl ∷ ∀ a. ReactComponent { | a }
|
||||
|
||||
foreign import svgImpl ∷ ∀ a. ReactComponent { | a }
|
||||
@ -171,14 +189,63 @@ type OnDragEnd =
|
||||
type OnDrag =
|
||||
(EffectFn2 Event PanInfo Unit |+| Undefined)
|
||||
|
||||
type WhileTap =
|
||||
TargetAndTransition |+| String |+| Undefined
|
||||
|
||||
type OnTap =
|
||||
(EffectFn2 Event TapInfo Unit |+| Undefined)
|
||||
|
||||
type OnTapStart =
|
||||
(EffectFn2 Event TapInfo Unit |+| Undefined)
|
||||
|
||||
type OnTapEnd =
|
||||
(EffectFn2 Event TapInfo Unit |+| Undefined)
|
||||
|
||||
type OnTapCancel =
|
||||
(EffectFn2 Event TapInfo Unit |+| Undefined)
|
||||
|
||||
onTapStart ∷ (Event -> TapInfo -> Effect Unit) -> OnTapStart
|
||||
onTapStart = cast <<< toEffectFn
|
||||
|
||||
onTapEnd ∷ (Event -> TapInfo -> Effect Unit) -> OnTapEnd
|
||||
onTapEnd = cast <<< toEffectFn
|
||||
|
||||
onTap ∷ (Event -> TapInfo -> Effect Unit) -> OnTap
|
||||
onTap fn2 = cast (mkEffectFn2 fn2)
|
||||
|
||||
onTapCancel ∷ (Event -> TapInfo -> Effect Unit) -> OnTap
|
||||
onTapCancel fn2 = cast (mkEffectFn2 fn2)
|
||||
|
||||
type TapInfo =
|
||||
{ x ∷ Number, y ∷ Number }
|
||||
|
||||
-- Can contain "transition" and "transitionEnd"
|
||||
type TargetAndTransition =
|
||||
CSS
|
||||
|
||||
type DragPropagation =
|
||||
Boolean |+| Undefined
|
||||
|
||||
whileTap ∷ ∀ c. Castable c WhileTap => c -> WhileTap
|
||||
whileTap = cast
|
||||
|
||||
class EffectFnMaker fn effectFn | fn -> effectFn where
|
||||
toEffectFn ∷ fn -> effectFn
|
||||
|
||||
instance callbackableEffectFn2 ∷ EffectFnMaker (a -> b -> Effect c) (EffectFn2 a b c) where
|
||||
toEffectFn = mkEffectFn2
|
||||
|
||||
instance callbackableEffectFn1 ∷ EffectFnMaker (a -> Effect b) (EffectFn1 a b) where
|
||||
toEffectFn = mkEffectFn1
|
||||
|
||||
callback ∷ ∀ a c f. Castable c a => EffectFnMaker f c => f -> a
|
||||
callback = cast <<< toEffectFn
|
||||
|
||||
onDragStart ∷ (Event -> PanInfo -> Effect Unit) -> OnDragStart
|
||||
onDragStart fn2 = cast (mkEffectFn2 fn2)
|
||||
onDragStart = cast <<< toEffectFn
|
||||
|
||||
onDragEnd ∷ (Event -> PanInfo -> Effect Unit) -> OnDragEnd
|
||||
onDragEnd fn2 = cast (mkEffectFn2 fn2)
|
||||
onDragEnd = cast <<< toEffectFn
|
||||
|
||||
onDrag ∷ (Event -> PanInfo -> Effect Unit) -> OnDrag
|
||||
onDrag fn2 = cast (mkEffectFn2 fn2)
|
||||
@ -198,6 +265,11 @@ type MotionPropsF f r =
|
||||
, transition ∷ f Transition
|
||||
, layout ∷ f Layout
|
||||
, layoutId ∷ f String |+| Undefined
|
||||
, whileTap ∷ f WhileTap
|
||||
, onTap ∷ f OnTap
|
||||
, onTapStart ∷ f OnTapStart
|
||||
, onTapEnd ∷ f OnTapEnd
|
||||
, onTapCancel ∷ f OnTapCancel
|
||||
, exit ∷ f Exit
|
||||
| r
|
||||
)
|
||||
@ -249,6 +321,9 @@ makeVariantLabels = hmapWithIndex MakeVariantLabel
|
||||
div ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_div) => ReactComponent { | attrs }
|
||||
div = divImpl
|
||||
|
||||
button ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + Props_button) => ReactComponent { | attrs }
|
||||
button = buttonImpl
|
||||
|
||||
svg ∷ ∀ attrs attrs_. Union attrs attrs_ (MotionProps + (SharedSVGProps Props_svg)) => ReactComponent { | attrs }
|
||||
svg = svgImpl
|
||||
|
||||
|
@ -6,4 +6,5 @@ exports.setImpl = v => render => mv => () => {
|
||||
mv.set(v, render)
|
||||
}
|
||||
exports.isAnimating = mv => () => { return mv.isAnimating() }
|
||||
exports.stop = mv => () => { return mv.stop() }
|
||||
exports.stop = mv => () => { return mv.stop() }
|
||||
exports.onChangeImpl = callback => mv => { return () => mv.onChange(callback); }
|
@ -2,7 +2,7 @@ module MotionValue where
|
||||
|
||||
import Prelude
|
||||
import Effect (Effect)
|
||||
import Effect.Uncurried (EffectFn1, runEffectFn1)
|
||||
import Effect.Uncurried (EffectFn1, mkEffectFn1, runEffectFn1)
|
||||
import React.Basic.Hooks (Hook, unsafeHook)
|
||||
|
||||
foreign import data MotionValue ∷ Type -> Type
|
||||
@ -21,9 +21,14 @@ setButDoNotRender v = setImpl v false
|
||||
set ∷ ∀ a. a -> MotionValue a -> Effect Unit
|
||||
set v = setImpl v true
|
||||
|
||||
foreign import isAnimating ∷ ∀ a. (MotionValue a) -> Effect Boolean
|
||||
foreign import isAnimating ∷ ∀ a. MotionValue a -> Effect Boolean
|
||||
|
||||
foreign import stop ∷ ∀ a. (MotionValue a) -> Effect Unit
|
||||
foreign import stop ∷ ∀ a. MotionValue a -> Effect Unit
|
||||
|
||||
foreign import onChangeImpl ∷ ∀ a. EffectFn1 a Unit -> MotionValue a -> Effect (Effect Unit)
|
||||
|
||||
onChange ∷ ∀ a. (a -> Effect Unit) -> MotionValue a -> Effect (Effect Unit)
|
||||
onChange = mkEffectFn1 >>> onChangeImpl
|
||||
|
||||
useMotionValue ∷ ∀ a. a -> Hook (UseMotionValue a) (MotionValue a)
|
||||
useMotionValue initialValue =
|
||||
|
3
src/React/Basic/Popper/Hook.js
Normal file
3
src/React/Basic/Popper/Hook.js
Normal file
@ -0,0 +1,3 @@
|
||||
const reactPopper = require('react-popper')
|
||||
|
||||
exports.usePopperImpl = reactPopper.usePopper
|
64
src/React/Basic/Popper/Hook.purs
Normal file
64
src/React/Basic/Popper/Hook.purs
Normal file
@ -0,0 +1,64 @@
|
||||
module React.Basic.Popper.Hook where
|
||||
|
||||
import Prelude
|
||||
import Data.String as String
|
||||
import Data.Tuple (Tuple(..))
|
||||
import Effect.Uncurried (EffectFn3, runEffectFn3)
|
||||
import Foreign.Object (Object)
|
||||
import Foreign.Object as Object
|
||||
import Prim.Row (class Union)
|
||||
import React.Basic.DOM (CSS)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Yoga.Prelude.View (Hook, NodeRef, unsafeHook)
|
||||
|
||||
foreign import data UsePopper ∷ Type -> Type -> Type
|
||||
|
||||
type ReferenceElement =
|
||||
NodeRef
|
||||
|
||||
type PopperElement =
|
||||
NodeRef
|
||||
|
||||
type ArrowElement =
|
||||
NodeRef
|
||||
|
||||
type DataAttributes =
|
||||
{ popper ∷ Object String, arrow ∷ Object String }
|
||||
|
||||
type Styles =
|
||||
{ popper ∷ CSS, arrow ∷ CSS }
|
||||
|
||||
type PopperData =
|
||||
{ styles ∷ Styles, attributes ∷ DataAttributes }
|
||||
|
||||
foreign import data Modifier ∷ Type
|
||||
|
||||
modifierArrow ∷ ArrowElement -> Modifier
|
||||
modifierArrow element = unsafeCoerce { name: "arrow", options: { element } }
|
||||
|
||||
modifierOffset ∷ { x ∷ Number, y ∷ Number } -> Modifier
|
||||
modifierOffset { x, y } = unsafeCoerce { name: "offset", options: { offset: [ x, y ] } }
|
||||
|
||||
type Options =
|
||||
( modifiers ∷ Array Modifier
|
||||
, strategy ∷ String
|
||||
)
|
||||
|
||||
foreign import usePopperImpl ∷ ∀ opts. EffectFn3 ReferenceElement PopperElement { | opts } PopperData
|
||||
|
||||
toDataAttributes ∷ Object String -> Object String
|
||||
toDataAttributes o = Object.toArrayWithKey (Tuple <<< dropPrefix) o # Object.fromFoldable
|
||||
where
|
||||
dropPrefix = String.drop (String.length "data-")
|
||||
|
||||
usePopper ∷ ∀ opts opts_. Union opts opts_ Options => ReferenceElement -> PopperElement -> { | opts } -> Hook (UsePopper { | opts }) PopperData
|
||||
usePopper elemRef popperRef opts =
|
||||
unsafeHook do
|
||||
result <- runEffectFn3 usePopperImpl elemRef popperRef opts
|
||||
pure
|
||||
$ result
|
||||
{ attributes
|
||||
{ popper = toDataAttributes result.attributes.popper
|
||||
, arrow = toDataAttributes result.attributes.arrow
|
||||
}
|
||||
}
|
205
src/React/Basic/Popper/Story.purs
Normal file
205
src/React/Basic/Popper/Story.purs
Normal file
@ -0,0 +1,205 @@
|
||||
module React.Basic.Popper.Story where
|
||||
|
||||
import Prelude
|
||||
import Color as Color
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Data.Nullable (null)
|
||||
import Data.Tuple.Nested ((/\))
|
||||
import Effect (Effect)
|
||||
import Effect.Uncurried (mkEffectFn1)
|
||||
import Effect.Unsafe (unsafePerformEffect)
|
||||
import Framer.Motion (onTap)
|
||||
import Framer.Motion as Motion
|
||||
import React.Basic (JSX, element, fragment)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Emotion as E
|
||||
import React.Basic.Hooks as React
|
||||
import React.Basic.Popper.Hook (modifierArrow, modifierOffset, usePopper)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Yoga.Block as Block
|
||||
import Yoga.Block.Atom.Toggle as Toggle
|
||||
import Yoga.Block.Container.Style (DarkOrLightMode(..))
|
||||
import Yoga.Block.Container.Style as Styles
|
||||
import Yoga.Block.Internal.CSS (nest)
|
||||
import Yoga.Prelude.View (NodeRef, el, styled, styledLeaf)
|
||||
|
||||
default ∷
|
||||
{ decorators ∷ Array (Effect JSX -> JSX)
|
||||
, title ∷ String
|
||||
}
|
||||
default =
|
||||
{ title: "Popper"
|
||||
, decorators:
|
||||
[ \storyFn ->
|
||||
R.div_
|
||||
[ element E.global { styles: Styles.global }
|
||||
, unsafePerformEffect storyFn
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
popper ∷ Effect JSX
|
||||
popper = do
|
||||
example <- mkBasicExample
|
||||
pure
|
||||
$ fragment
|
||||
[ R.div_
|
||||
[ R.h2_ [ R.text "Some Popper" ]
|
||||
, element example {}
|
||||
]
|
||||
]
|
||||
where
|
||||
nullRef ∷ NodeRef
|
||||
nullRef = unsafeCoerce null
|
||||
|
||||
mkBasicExample =
|
||||
React.reactComponent "Popper example" \p -> React.do
|
||||
referenceElement /\ setReferenceElement <- React.useState' nullRef
|
||||
popperElement /\ setPopperElement <- React.useState' nullRef
|
||||
arrowElement /\ setArrowElement <- React.useState' nullRef
|
||||
{ styles, attributes } <-
|
||||
usePopper referenceElement popperElement
|
||||
{ modifiers:
|
||||
[ modifierArrow arrowElement
|
||||
, modifierOffset { x: 0.0, y: 8.0 }
|
||||
]
|
||||
}
|
||||
pure
|
||||
$ fragment
|
||||
$ [ element R.button'
|
||||
{ type: "button"
|
||||
, ref: unsafeCoerce (mkEffectFn1 setReferenceElement)
|
||||
, children: [ R.text "Reference element" ]
|
||||
}
|
||||
, styled R.div'
|
||||
{ className: "popper-element"
|
||||
, css:
|
||||
E.css
|
||||
{ background: E.str "darkslateblue"
|
||||
, borderRadius: E.str "8px"
|
||||
, display: E.str "block"
|
||||
, padding: E.str "4px 8px"
|
||||
, "&[data-popper-placement^='top'] > .popper-arrow":
|
||||
nest { bottom: E.str "-4px" }
|
||||
, "&[data-popper-placement^='bottom'] > .popper-arrow":
|
||||
nest { top: E.str "-4px" }
|
||||
, "&[data-popper-placement^='left'] > .popper-arrow":
|
||||
nest { right: E.str "-4px" }
|
||||
, "&[data-popper-placement^='right'] > .popper-arrow":
|
||||
nest { left: E.str "-4px" }
|
||||
}
|
||||
, ref: unsafeCoerce (mkEffectFn1 setPopperElement)
|
||||
, style: styles.popper
|
||||
, _data: attributes.popper
|
||||
}
|
||||
[ R.text "Popper Element"
|
||||
, styledLeaf R.div'
|
||||
{ className: "popper-arrow"
|
||||
, id: "arrow"
|
||||
, css:
|
||||
E.css
|
||||
{ position: E.str "absolute"
|
||||
, width: E.str "8px"
|
||||
, height: E.str "8px"
|
||||
, zIndex: E.str "-1"
|
||||
, "&::before":
|
||||
nest
|
||||
{ position: E.str "absolute"
|
||||
, width: E.str "8px"
|
||||
, height: E.str "8px"
|
||||
, zIndex: E.str "-1"
|
||||
, content: E.str "''"
|
||||
, transform: E.str "rotate(45deg)"
|
||||
, background: E.str "darkslateblue"
|
||||
}
|
||||
}
|
||||
, ref: unsafeCoerce (mkEffectFn1 setArrowElement)
|
||||
, style: styles.arrow
|
||||
, _data: attributes.arrow
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
animatedPopper ∷ Effect JSX
|
||||
animatedPopper = do
|
||||
example <- mkBasicExample
|
||||
pure
|
||||
$ fragment
|
||||
[ R.div_
|
||||
[ R.h2_ [ R.text "Some Popper" ]
|
||||
, element example {}
|
||||
]
|
||||
]
|
||||
where
|
||||
nullRef ∷ NodeRef
|
||||
nullRef = unsafeCoerce null
|
||||
|
||||
mkBasicExample =
|
||||
React.reactComponent "Popper example" \p -> React.do
|
||||
referenceElement /\ setReferenceElement <- React.useState' nullRef
|
||||
popperElement /\ setPopperElement <- React.useState' nullRef
|
||||
arrowElement /\ setArrowElement <- React.useState' nullRef
|
||||
{ styles, attributes } <-
|
||||
usePopper referenceElement popperElement
|
||||
{ modifiers:
|
||||
[ modifierArrow arrowElement
|
||||
, modifierOffset { x: 0.0, y: 8.0 }
|
||||
]
|
||||
}
|
||||
pure
|
||||
$ fragment
|
||||
$ [ element R.div'
|
||||
{ ref: unsafeCoerce (mkEffectFn1 setReferenceElement)
|
||||
, children: [ R.text "Reference element" ]
|
||||
}
|
||||
, el Motion.animatePresence {}
|
||||
[ styled Motion.div
|
||||
{ className: "popper-element"
|
||||
, key: "heinz"
|
||||
, css:
|
||||
E.css
|
||||
{ background: E.str "darkslateblue"
|
||||
, borderRadius: E.str "8px"
|
||||
, display: E.str "block"
|
||||
, padding: E.str "4px 8px"
|
||||
, "&[data-popper-placement^='top'] > .popper-arrow":
|
||||
nest { bottom: E.str "-4px" }
|
||||
, "&[data-popper-placement^='bottom'] > .popper-arrow":
|
||||
nest { top: E.str "-4px" }
|
||||
, "&[data-popper-placement^='left'] > .popper-arrow":
|
||||
nest { right: E.str "-4px" }
|
||||
, "&[data-popper-placement^='right'] > .popper-arrow":
|
||||
nest { left: E.str "-4px" }
|
||||
}
|
||||
, ref: unsafeCoerce (mkEffectFn1 setPopperElement)
|
||||
, style: styles.popper
|
||||
, _data: attributes.popper
|
||||
}
|
||||
[ R.text "Popper Element"
|
||||
, styledLeaf R.div'
|
||||
{ className: "popper-arrow"
|
||||
, id: "arrow"
|
||||
, css:
|
||||
E.css
|
||||
{ position: E.str "absolute"
|
||||
, width: E.str "8px"
|
||||
, height: E.str "8px"
|
||||
, zIndex: E.str "-1"
|
||||
, "&::before":
|
||||
nest
|
||||
{ position: E.str "absolute"
|
||||
, width: E.str "8px"
|
||||
, height: E.str "8px"
|
||||
, zIndex: E.str "-1"
|
||||
, content: E.str "''"
|
||||
, transform: E.str "rotate(45deg)"
|
||||
, background: E.str "darkslateblue"
|
||||
}
|
||||
}
|
||||
, ref: unsafeCoerce (mkEffectFn1 setArrowElement)
|
||||
, style: styles.arrow
|
||||
, _data: attributes.arrow
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
@ -28,6 +28,7 @@ el_ x props = Hooks.element x props
|
||||
styled ∷
|
||||
∀ props.
|
||||
Lacks "children" props =>
|
||||
Lacks "keyp" props =>
|
||||
ReactComponent { className ∷ String, children ∷ Array JSX | props } ->
|
||||
{ className ∷ String, css ∷ Emotion.Style | props } -> Array JSX -> JSX
|
||||
styled x props children = Emotion.element x (Record.insert (SProxy ∷ SProxy "children") children props)
|
||||
|
@ -22,7 +22,7 @@ centre = Centre.component
|
||||
cluster ∷ ∀ p q. Union p q Cluster.Props => ReactComponent { | p }
|
||||
cluster = Cluster.component
|
||||
|
||||
container ∷ ReactComponent Container.Props
|
||||
container ∷ ∀ p q. Union p q Container.Props => ReactComponent { | p }
|
||||
container = Container.component
|
||||
|
||||
imposter ∷ ∀ p q. Union p q Imposter.Props => ReactComponent { | p }
|
||||
@ -34,7 +34,7 @@ modal = Modal.component
|
||||
range ∷ ∀ p q. Union p q Range.Props => ReactComponent { | p }
|
||||
range = Range.component
|
||||
|
||||
segmented ∷ ReactComponent Segmented.ComponentProps
|
||||
segmented ∷ ReactComponent Segmented.Props
|
||||
segmented = Segmented.component
|
||||
|
||||
sidebar ∷ ∀ p q. Union p q Sidebar.Props => ReactComponent { | p }
|
||||
|
@ -2,7 +2,6 @@ module Yoga.Block.Atom.Range.Style where
|
||||
|
||||
import Yoga.Prelude.Style
|
||||
import Data.Interpolate (i)
|
||||
import React.Basic.Emotion (inlineBlock)
|
||||
import Yoga.Block.Container.Style (colour)
|
||||
|
||||
type Props f r =
|
||||
|
@ -2,4 +2,4 @@ module Yoga.Block.Atom.Segmented
|
||||
( module Yoga.Block.Atom.Segmented.View
|
||||
) where
|
||||
|
||||
import Yoga.Block.Atom.Segmented.View (component, Props, ComponentProps)
|
||||
import Yoga.Block.Atom.Segmented.View (component, Props, Item)
|
||||
|
@ -15,6 +15,7 @@ cluster =
|
||||
{ overflow: auto
|
||||
, flex: str "1"
|
||||
, display: str "inline-flex"
|
||||
, userSelect: none
|
||||
}
|
||||
|
||||
segmented ∷ Style
|
||||
@ -30,27 +31,25 @@ segmented = styles
|
||||
, minHeight: str "min-content"
|
||||
, background: str colour.background10
|
||||
, boxShadow: str "inset 0 1 0 rgba(0,0,0,0.1)"
|
||||
, borderRadius: str "10px"
|
||||
, borderRadius: var "--s-1"
|
||||
, border: str $ i "1px solid " colour.background15
|
||||
, borderBottom: str $ i "1px solid " colour.background20
|
||||
, padding: _0
|
||||
, overflow: scroll
|
||||
, userSelect: none
|
||||
}
|
||||
|
||||
activeElement ∷ Style
|
||||
activeElement =
|
||||
css
|
||||
{ position: absolute
|
||||
, borderRadius: str "8px"
|
||||
, borderRadius: str "calc(var(--s-1) * 0.75)"
|
||||
, background: str colour.interfaceBackground
|
||||
, border: str $ i "1px solid " colour.interfaceBackgroundShadow
|
||||
, borderTop: str $ i "1px solid " colour.interfaceBackgroundHighlight
|
||||
, borderBottom: str $ i "1px solid " colour.interfaceBackgroundShadow
|
||||
, boxShadow: str "0 0 1px rgba(0,0,0,0.55)"
|
||||
, margin: _0
|
||||
, boxShadow: str "0 1px 2px rgba(20,20,20,0.67)"
|
||||
, padding: _0
|
||||
, zIndex: str "3"
|
||||
, cursor: str "grab"
|
||||
}
|
||||
|
||||
button ∷ { isFirst ∷ Boolean, isLast ∷ Boolean } -> Style
|
||||
@ -60,17 +59,21 @@ button { isFirst, isLast } =
|
||||
, appearance: none
|
||||
, color: str colour.text
|
||||
, border: none
|
||||
, margin: _0
|
||||
, padding: _0
|
||||
, fontSize: str "var(--s0)"
|
||||
, marginLeft: str if isFirst then "1px" else "0"
|
||||
, marginRight: str if isLast then "1px" else "0"
|
||||
, boxSizing: borderBox
|
||||
, zIndex: str "3"
|
||||
, minWidth: var "--s3"
|
||||
, "&:active": nest { outline: str "0" }
|
||||
, "&:focus": nest { outline: none }
|
||||
, "&:focus > .ry-segmented-button__content":
|
||||
nest
|
||||
{ borderColor: str colour.highlight
|
||||
, transition: str "all 0.083s ease 0.083s"
|
||||
}
|
||||
, userSelect: none
|
||||
}
|
||||
|
||||
buttonContent ∷ { isFirst ∷ Boolean, isLast ∷ Boolean } -> Style
|
||||
@ -78,16 +81,18 @@ buttonContent { isFirst, isLast } =
|
||||
css
|
||||
{ "&:active": nest { outline: str "0" }
|
||||
, "&:focus": nest { outline: none }
|
||||
, paddingTop: str "1px"
|
||||
, paddingBottom: str "1px"
|
||||
, paddingLeft: str if isFirst then edgePadding else inBetweenPadding
|
||||
, paddingRight: str if isLast then edgePadding else inBetweenPadding
|
||||
, borderRadius: str "8px"
|
||||
, borderRadius: str "calc(var(--s-1) * 0.75)"
|
||||
, border: str $ i borderSize " solid transparent"
|
||||
, display: flex
|
||||
, alignItems: center
|
||||
, outlineRight: str "1px solid red"
|
||||
, justifyContent: center
|
||||
, margin: _0
|
||||
, minHeight: _100percent
|
||||
, overflow: visible
|
||||
, userSelect: none
|
||||
}
|
||||
where
|
||||
borderSize = "var(--s-4)"
|
||||
|
@ -1,14 +1,7 @@
|
||||
module Yoga.Block.Atom.Segmented.View (component, Props, Item, MandatoryProps, PropsF, ComponentProps) where
|
||||
module Yoga.Block.Atom.Segmented.View (component, Item, Props) where
|
||||
|
||||
import Yoga.Prelude.View
|
||||
import Control.Alt ((<|>))
|
||||
import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.MonadZero as MZ
|
||||
import Data.Array as A
|
||||
import Data.FoldableWithIndex (foldMapWithIndex)
|
||||
import Data.FunctorWithIndex (mapWithIndex)
|
||||
import Data.Maybe (fromMaybe', isJust, maybe)
|
||||
import Data.Newtype (wrap)
|
||||
import Data.Time.Duration (Milliseconds(..))
|
||||
import Data.Traversable (traverse)
|
||||
@ -17,15 +10,9 @@ import Data.TwoOrMore as TwoOrMore
|
||||
import Effect.Aff (delay)
|
||||
import Effect.Unsafe (unsafePerformEffect)
|
||||
import Foreign.Object as Object
|
||||
import Framer.Motion (VariantLabel)
|
||||
import Framer.Motion as Motion
|
||||
import Hooks.Key as Key
|
||||
import Hooks.Scroll (useScrollPosition)
|
||||
import Hooks.UseResize (useResize)
|
||||
import Literals.Undefined (undefined)
|
||||
import MotionValue (useMotionValue)
|
||||
import MotionValue as MotionValue
|
||||
import Partial.Unsafe (unsafeCrashWith)
|
||||
import Math as Math
|
||||
import React.Basic.DOM (css)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Emotion as E
|
||||
@ -33,214 +20,23 @@ import React.Basic.Extra.Hooks.UseKeyDown (useKeyDown)
|
||||
import React.Basic.Hooks (reactComponent)
|
||||
import React.Basic.Hooks as React
|
||||
import React.Basic.Hooks.Aff (useAff)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Untagged.Castable (cast)
|
||||
import Web.HTML.HTMLElement (HTMLElement, blur, focus, getBoundingClientRect)
|
||||
import Web.HTML.HTMLElement as HTMLElement
|
||||
import Yoga.Block.Atom.Segmented.Style as Style
|
||||
|
||||
type PropsF f =
|
||||
(
|
||||
| MandatoryProps + Style.Props f ()
|
||||
)
|
||||
|
||||
type MandatoryProps r =
|
||||
( activeItemRefs ∷ TwoOrMore (Ref (Nullable Node))
|
||||
, activeItemIndex ∷ Int
|
||||
, updateActiveIndex ∷ Int -> Effect Unit
|
||||
| r
|
||||
)
|
||||
|
||||
type Props =
|
||||
PropsF Id
|
||||
|
||||
activeComponent ∷ ∀ p p_. Union (MandatoryProps p) p_ Props => ReactComponent { | MandatoryProps p }
|
||||
activeComponent = rawActiveComponent
|
||||
|
||||
indexToVariant ∷ Int -> VariantLabel
|
||||
indexToVariant = show >>> unsafeCoerce
|
||||
|
||||
type BBox =
|
||||
{ top ∷ Number, left ∷ Number, width ∷ Number, height ∷ Number }
|
||||
|
||||
findOverlapping ∷ Int -> TwoOrMore BBox -> Number -> Int
|
||||
findOverlapping activeIndex styles x =
|
||||
fromMaybe activeIndex do
|
||||
curr <- styles TwoOrMore.!! activeIndex
|
||||
let fst = TwoOrMore.head styles
|
||||
let lst = TwoOrMore.last styles
|
||||
let inside e = (e.left < x) && (e.left + e.width) >= x
|
||||
let tooFarLeft = MZ.guard (x <= fst.left + fst.width) $> 0
|
||||
let tooFarRight = MZ.guard (x >= lst.left) $> TwoOrMore.length styles - 1
|
||||
TwoOrMore.findIndex inside styles <|> tooFarLeft <|> tooFarRight
|
||||
|
||||
handleDrag ∷
|
||||
{ activeItemIndex ∷ Int
|
||||
, animationVariants ∷ TwoOrMore BBox
|
||||
, x ∷ Number
|
||||
} ->
|
||||
{ left ∷ Number, width ∷ Number }
|
||||
handleDrag { x, activeItemIndex, animationVariants } = do
|
||||
let idx = findOverlapping activeItemIndex animationVariants x
|
||||
let av = animationVariants
|
||||
let firstVariant = av # TwoOrMore.head
|
||||
let lastVariant = av # TwoOrMore.last
|
||||
let baseVariant = av TwoOrMore.!! idx # fromMaybe' \_ -> unsafeCrashWith "shit"
|
||||
let
|
||||
closestVariant =
|
||||
if x >= (baseVariant.left + (baseVariant.width / 2.0)) then
|
||||
av TwoOrMore.!! (idx + 1) # fromMaybe lastVariant
|
||||
else
|
||||
av TwoOrMore.!! (idx - 1) # fromMaybe firstVariant
|
||||
let
|
||||
greater /\ smaller =
|
||||
if baseVariant.left > closestVariant.left then
|
||||
baseVariant /\ closestVariant
|
||||
else
|
||||
closestVariant /\ baseVariant
|
||||
-- Total
|
||||
let rangeStart = smaller.left + (smaller.width / 2.0)
|
||||
let rangeEnd = greater.left + (greater.width / 2.0)
|
||||
let range = rangeEnd - rangeStart
|
||||
let ratio = ((x - rangeStart) / range)
|
||||
let interpolatedWidth = (greater.width * ratio) + smaller.width * (1.0 - ratio)
|
||||
-- Right
|
||||
let rangeStartRight = smaller.left + smaller.width
|
||||
let rangeEndRight = greater.left + greater.width
|
||||
let rangeRight = rangeEndRight - rangeStartRight
|
||||
let ratioRight = ((x + (interpolatedWidth / 2.0) - rangeStartRight) / rangeRight)
|
||||
-- Left
|
||||
let rangeStartLeft = smaller.left
|
||||
let rangeEndLeft = greater.left
|
||||
let rangeLeft = rangeEndLeft - rangeStartLeft
|
||||
let ratioLeft = (((x - (interpolatedWidth / 2.0)) - rangeStartLeft) / rangeLeft)
|
||||
-- Individual
|
||||
let left = rangeStartLeft + (ratioLeft * rangeLeft)
|
||||
let right = rangeStartRight + (ratioRight * rangeRight)
|
||||
let width = right - left
|
||||
if x < firstVariant.left then
|
||||
{ left: firstVariant.left, width: firstVariant.width }
|
||||
else
|
||||
if x >= lastVariant.left + lastVariant.width then do
|
||||
{ left: lastVariant.left, width: lastVariant.width }
|
||||
else do
|
||||
{ left, width }
|
||||
|
||||
rawActiveComponent ∷ ∀ p. ReactComponent { | p }
|
||||
rawActiveComponent =
|
||||
mkForwardRefComponent "SegmentedActive" do
|
||||
\(props ∷ { | Props }) ref -> React.do
|
||||
maybeAnimationVariants /\ setVariants <- useState' Nothing
|
||||
maybeDragX /\ setDragX <- useState' Nothing
|
||||
{ scrollX } <- useScrollPosition
|
||||
activeLeft <- useMotionValue 0.0
|
||||
activeWidth <- useMotionValue 0.0
|
||||
useEffectAlways do
|
||||
_ <-
|
||||
runMaybeT do
|
||||
rawStyles <- traverse getStyle props.activeItemRefs
|
||||
let styles = rawStyles <#> \s -> s { left = s.left + scrollX }
|
||||
unless (maybeAnimationVariants == Just styles) do
|
||||
setVariants (Just styles) # lift
|
||||
mempty
|
||||
useLayoutEffect maybeDragX do
|
||||
case maybeDragX, maybeAnimationVariants of
|
||||
Just x, Just animationVariants -> do
|
||||
let { left, width } = handleDrag { activeItemIndex: props.activeItemIndex, animationVariants, x }
|
||||
activeLeft # MotionValue.set left
|
||||
activeWidth # MotionValue.set width
|
||||
_, _ -> mempty
|
||||
mempty
|
||||
let
|
||||
variants ∷ Motion.Variants
|
||||
variants = case maybeAnimationVariants of
|
||||
Just animationVariants ->
|
||||
animationVariants
|
||||
# foldMapWithIndex (\i s -> Object.singleton (show i) (css s))
|
||||
# Motion.variantsFromObject
|
||||
Nothing -> (cast undefined) ∷ Motion.Variants
|
||||
pure $ maybeAnimationVariants
|
||||
# foldMap \animationVariants ->
|
||||
styledLeaf Motion.div
|
||||
{ css: Style.activeElement
|
||||
, variants
|
||||
, className: "ry-active-segmented-element"
|
||||
, initial: Motion.initial (indexToVariant props.activeItemIndex)
|
||||
, drag: Motion.drag "x"
|
||||
, dragMomentum: Motion.dragMomentum false
|
||||
, animate: Motion.animate $ indexToVariant props.activeItemIndex
|
||||
, layout: Motion.layout true
|
||||
, style:
|
||||
css
|
||||
{ left: activeLeft
|
||||
, width: activeWidth
|
||||
}
|
||||
, onDragStart:
|
||||
Motion.onDragStart \_ pi -> do
|
||||
setDragX (Just pi.point.x)
|
||||
, onDrag:
|
||||
Motion.onDrag \_ pi -> do
|
||||
when (isJust maybeDragX) $ setDragX (Just pi.point.x)
|
||||
, onDragEnd:
|
||||
Motion.onDragEnd \_ pi -> do
|
||||
let x = maybeDragX # fromMaybe' \_ -> unsafeCrashWith "no x should not happen"
|
||||
let newIdx = findOverlapping props.activeItemIndex animationVariants x
|
||||
let v = animationVariants TwoOrMore.!! newIdx # fromMaybe' \_ -> unsafeCrashWith "omg"
|
||||
setDragX Nothing
|
||||
activeLeft # MotionValue.set v.left
|
||||
activeWidth # MotionValue.set v.width
|
||||
props.updateActiveIndex newIdx
|
||||
, dragConstraints: Motion.dragConstraints { left: 0, right: 0 }
|
||||
, dragElastic: Motion.dragElastic false
|
||||
, transition:
|
||||
Motion.transition
|
||||
{ type: "tween", duration: if isJust maybeDragX then 0.0 else 0.167, ease: "easeOut" }
|
||||
, _aria: Object.fromHomogeneous { hidden: "true" }
|
||||
, ref
|
||||
}
|
||||
|
||||
getStyle ∷
|
||||
Ref (Nullable Node) ->
|
||||
MaybeT Effect BBox
|
||||
getStyle itemRef = do
|
||||
node <- MaybeT $ readRefMaybe itemRef
|
||||
htmlElement <- MaybeT $ pure $ HTMLElement.fromNode node
|
||||
br <- lift $ getBoundingClientRect htmlElement
|
||||
pure
|
||||
{ width: br.width
|
||||
, height: br.height
|
||||
, left: br.left
|
||||
, top: br.top
|
||||
}
|
||||
import Yoga.Block.Atom.Segmented.View.ActiveIndicator as ActiveIndicator
|
||||
|
||||
type Item =
|
||||
{ id ∷ String, value ∷ String }
|
||||
|
||||
type ComponentProps =
|
||||
type Props =
|
||||
{ buttonContents ∷ TwoOrMore Item
|
||||
, activeIndex ∷ Int
|
||||
, updateActiveIndex ∷ Int -> Effect Unit
|
||||
}
|
||||
|
||||
getHTMLElementAtIndex ∷ Int -> TwoOrMore (NodeRef) -> Effect (Maybe HTMLElement)
|
||||
getHTMLElementAtIndex idx refs =
|
||||
runMaybeT do
|
||||
ref <- refs TwoOrMore.!! idx # pure >>> wrap
|
||||
node <- React.readRefMaybe ref # wrap
|
||||
HTMLElement.fromNode node # pure >>> wrap
|
||||
|
||||
blurAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
|
||||
blurAtIndex idx refs = do
|
||||
getHTMLElementAtIndex idx refs >>= traverse_ blur
|
||||
|
||||
focusAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
|
||||
focusAtIndex idx refs = do
|
||||
getHTMLElementAtIndex idx refs >>= traverse_ focus
|
||||
|
||||
component ∷ ReactComponent ComponentProps
|
||||
component ∷ ReactComponent Props
|
||||
component =
|
||||
unsafePerformEffect
|
||||
$ reactComponent "Segmented" \({ buttonContents, activeIndex, updateActiveIndex } ∷ ComponentProps) -> React.do
|
||||
$ reactComponent "Segmented" \({ buttonContents, activeIndex, updateActiveIndex } ∷ Props) -> React.do
|
||||
-------------------------------------------
|
||||
-- Store button refs for animation purposes
|
||||
itemRefs /\ setItemRefs ∷ Maybe (TwoOrMore _) /\ _ <- useState' Nothing
|
||||
@ -248,6 +44,7 @@ component =
|
||||
refs <- traverse (const createRef) buttonContents
|
||||
setItemRefs (Just refs)
|
||||
mempty
|
||||
windowWidth /\ setWindowWidth <- useState' 0.0
|
||||
-------------------------------------------
|
||||
-- Support keyboard input
|
||||
let
|
||||
@ -272,10 +69,15 @@ component =
|
||||
-- Ensure redraw on window resize
|
||||
windowSize <- useResize
|
||||
useAff { windowSize } do
|
||||
delay (200.0 # Milliseconds)
|
||||
delay
|
||||
if Math.abs (windowWidth - windowSize.width) < 10.0 then
|
||||
100.0 # Milliseconds
|
||||
else
|
||||
10.0 # Milliseconds
|
||||
liftEffect do -- force rerender
|
||||
refs <- traverse (const createRef) buttonContents
|
||||
setItemRefs (Just refs)
|
||||
setWindowWidth windowSize.width
|
||||
let
|
||||
children ∷ Array JSX
|
||||
children = refsAndContents <#> mapWithIndex contentToChild # maybe mempty TwoOrMore.toArray
|
||||
@ -320,12 +122,28 @@ component =
|
||||
, children:
|
||||
itemRefs
|
||||
# foldMap \activeItemRefs ->
|
||||
React.element activeComponent
|
||||
React.element ActiveIndicator.component
|
||||
{ activeItemRefs
|
||||
, activeItemIndex: activeIndex
|
||||
, updateActiveIndex
|
||||
, windowWidth
|
||||
}
|
||||
A.: children
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
getHTMLElementAtIndex ∷ Int -> TwoOrMore (NodeRef) -> Effect (Maybe HTMLElement)
|
||||
getHTMLElementAtIndex idx refs =
|
||||
runMaybeT do
|
||||
ref <- refs TwoOrMore.!! idx # pure >>> wrap
|
||||
node <- React.readRefMaybe ref # wrap
|
||||
HTMLElement.fromNode node # pure >>> wrap
|
||||
|
||||
blurAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
|
||||
blurAtIndex idx refs = do
|
||||
getHTMLElementAtIndex idx refs >>= traverse_ blur
|
||||
|
||||
focusAtIndex ∷ Int -> TwoOrMore (Ref (Nullable Node)) -> Effect Unit
|
||||
focusAtIndex idx refs = do
|
||||
getHTMLElementAtIndex idx refs >>= traverse_ focus
|
||||
|
185
src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs
Normal file
185
src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs
Normal file
@ -0,0 +1,185 @@
|
||||
module Yoga.Block.Atom.Segmented.View.ActiveIndicator (Props, component) where
|
||||
|
||||
import Yoga.Prelude.View
|
||||
import Control.MonadZero as MZ
|
||||
import Data.Traversable (traverse)
|
||||
import Data.TwoOrMore (TwoOrMore)
|
||||
import Data.TwoOrMore as TwoOrMore
|
||||
import Effect.Unsafe (unsafePerformEffect)
|
||||
import Foreign.Object as Object
|
||||
import Framer.Motion (VariantLabel)
|
||||
import Framer.Motion as Motion
|
||||
import Hooks.Scroll (useScrollPosition)
|
||||
import Literals.Undefined (undefined)
|
||||
import MotionValue (useMotionValue)
|
||||
import MotionValue as MotionValue
|
||||
import Partial.Unsafe (unsafeCrashWith)
|
||||
import React.Basic.DOM (css)
|
||||
import React.Basic.Emotion as Emotion
|
||||
import React.Basic.Hooks (reactComponent)
|
||||
import React.Basic.Hooks as React
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Yoga.Block.Atom.Segmented.Style as Style
|
||||
|
||||
type Props =
|
||||
{ activeItemRefs ∷ TwoOrMore (Ref (Nullable Node))
|
||||
, activeItemIndex ∷ Int
|
||||
, updateActiveIndex ∷ Int -> Effect Unit
|
||||
, windowWidth ∷ Number
|
||||
}
|
||||
|
||||
component ∷ ReactComponent Props
|
||||
component =
|
||||
unsafePerformEffect
|
||||
$ reactComponent "SegmentedActive" do
|
||||
\(props ∷ Props) -> React.do
|
||||
maybeAnimationVariants /\ setVariants <- useState' Nothing
|
||||
maybeDragX /\ setDragX <- useState' Nothing
|
||||
{ scrollX } <- useScrollPosition
|
||||
activeLeft <- useMotionValue 0.0
|
||||
activeWidth <- useMotionValue 0.0
|
||||
useEffectAlways do
|
||||
_ <-
|
||||
runMaybeT do
|
||||
rawStyles <- traverse getStyle props.activeItemRefs
|
||||
let styles = rawStyles <#> \s -> s { left = s.left + scrollX }
|
||||
unless (maybeAnimationVariants == Just styles) do
|
||||
setVariants (Just styles) # lift
|
||||
mempty
|
||||
useLayoutEffect maybeDragX do
|
||||
case maybeDragX, maybeAnimationVariants of
|
||||
Just x, Just animationVariants -> do
|
||||
let { left, width } = handleDrag { activeItemIndex: props.activeItemIndex, animationVariants, x }
|
||||
activeLeft # MotionValue.set left
|
||||
activeWidth # MotionValue.set width
|
||||
_, _ -> mempty
|
||||
mempty
|
||||
let
|
||||
variants ∷ Motion.Variants
|
||||
variants = case maybeAnimationVariants of
|
||||
Just animationVariants ->
|
||||
animationVariants
|
||||
# foldMapWithIndex (\i s -> Object.singleton (show i) (css s))
|
||||
# Motion.variantsFromObject
|
||||
Nothing -> (cast undefined) ∷ Motion.Variants
|
||||
pure $ maybeAnimationVariants
|
||||
# foldMap \animationVariants ->
|
||||
Emotion.elementKeyed Motion.div
|
||||
{ css: Style.activeElement
|
||||
, key: show props.windowWidth -- to force rerender
|
||||
, variants
|
||||
, className: "ry-active-segmented-element"
|
||||
, initial: Motion.initial (indexToVariant props.activeItemIndex)
|
||||
, drag: Motion.drag "x"
|
||||
, dragMomentum: Motion.dragMomentum false
|
||||
, animate: Motion.animate $ indexToVariant props.activeItemIndex
|
||||
, layout: Motion.layout true
|
||||
, whileTap: Motion.whileTap $ css { scale: 0.9 }
|
||||
, style:
|
||||
css
|
||||
{ left: activeLeft
|
||||
, width: activeWidth
|
||||
}
|
||||
, onDragStart:
|
||||
Motion.onDragStart \_ pi -> do
|
||||
setDragX (Just pi.point.x)
|
||||
, onDrag:
|
||||
Motion.onDrag \_ pi -> do
|
||||
when (isJust maybeDragX) $ setDragX (Just pi.point.x)
|
||||
, onDragEnd:
|
||||
Motion.onDragEnd \_ pi -> do
|
||||
let x = maybeDragX # fromMaybe' \_ -> unsafeCrashWith "no x should not happen"
|
||||
let newIdx = findOverlapping props.activeItemIndex animationVariants x
|
||||
let v = animationVariants TwoOrMore.!! newIdx # fromMaybe' \_ -> unsafeCrashWith "omg"
|
||||
setDragX Nothing
|
||||
activeLeft # MotionValue.set v.left
|
||||
activeWidth # MotionValue.set v.width
|
||||
props.updateActiveIndex newIdx
|
||||
, dragConstraints: Motion.dragConstraints { left: 0, right: 0 }
|
||||
, dragElastic: Motion.dragElastic false
|
||||
, transition:
|
||||
Motion.transition
|
||||
{ type: "tween", duration: if isJust maybeDragX then 0.0 else 0.167, ease: "easeOut" }
|
||||
, _aria: Object.fromHomogeneous { hidden: "true" }
|
||||
}
|
||||
|
||||
getStyle ∷
|
||||
Ref (Nullable Node) ->
|
||||
MaybeT Effect BBox
|
||||
getStyle itemRef = do
|
||||
br <- MaybeT $ getBoundingBoxFromRef itemRef
|
||||
pure
|
||||
{ width: br.width
|
||||
, height: br.height
|
||||
, left: br.left
|
||||
, top: br.top
|
||||
}
|
||||
|
||||
indexToVariant ∷ Int -> VariantLabel
|
||||
indexToVariant = show >>> unsafeCoerce
|
||||
|
||||
type BBox =
|
||||
{ top ∷ Number, left ∷ Number, width ∷ Number, height ∷ Number }
|
||||
|
||||
findOverlapping ∷ Int -> TwoOrMore BBox -> Number -> Int
|
||||
findOverlapping activeIndex styles x =
|
||||
fromMaybe activeIndex do
|
||||
curr <- styles TwoOrMore.!! activeIndex
|
||||
let fst = TwoOrMore.head styles
|
||||
let lst = TwoOrMore.last styles
|
||||
let inside e = (e.left < x) && (e.left + e.width) >= x
|
||||
let tooFarLeft = MZ.guard (x <= fst.left + fst.width) $> 0
|
||||
let tooFarRight = MZ.guard (x >= lst.left) $> TwoOrMore.length styles - 1
|
||||
TwoOrMore.findIndex inside styles <|> tooFarLeft <|> tooFarRight
|
||||
|
||||
handleDrag ∷
|
||||
{ activeItemIndex ∷ Int
|
||||
, animationVariants ∷ TwoOrMore BBox
|
||||
, x ∷ Number
|
||||
} ->
|
||||
{ left ∷ Number, width ∷ Number }
|
||||
handleDrag { x, activeItemIndex, animationVariants } = do
|
||||
let idx = findOverlapping activeItemIndex animationVariants x
|
||||
let av = animationVariants
|
||||
let firstVariant = av # TwoOrMore.head
|
||||
let lastVariant = av # TwoOrMore.last
|
||||
let baseVariant = av TwoOrMore.!! idx # fromMaybe' \_ -> unsafeCrashWith "shit"
|
||||
let
|
||||
closestVariant =
|
||||
if x >= (baseVariant.left + (baseVariant.width / 2.0)) then
|
||||
av TwoOrMore.!! (idx + 1) # fromMaybe lastVariant
|
||||
else
|
||||
av TwoOrMore.!! (idx - 1) # fromMaybe firstVariant
|
||||
let
|
||||
greater /\ smaller =
|
||||
if baseVariant.left > closestVariant.left then
|
||||
baseVariant /\ closestVariant
|
||||
else
|
||||
closestVariant /\ baseVariant
|
||||
-- Total
|
||||
let rangeStart = smaller.left + (smaller.width / 2.0)
|
||||
let rangeEnd = greater.left + (greater.width / 2.0)
|
||||
let range = rangeEnd - rangeStart
|
||||
let ratio = ((x - rangeStart) / range)
|
||||
let interpolatedWidth = (greater.width * ratio) + smaller.width * (1.0 - ratio)
|
||||
-- Right
|
||||
let rangeStartRight = smaller.left + smaller.width
|
||||
let rangeEndRight = greater.left + greater.width
|
||||
let rangeRight = rangeEndRight - rangeStartRight
|
||||
let ratioRight = ((x + (interpolatedWidth / 2.0) - rangeStartRight) / rangeRight)
|
||||
-- Left
|
||||
let rangeStartLeft = smaller.left
|
||||
let rangeEndLeft = greater.left
|
||||
let rangeLeft = rangeEndLeft - rangeStartLeft
|
||||
let ratioLeft = (((x - (interpolatedWidth / 2.0)) - rangeStartLeft) / rangeLeft)
|
||||
-- Individual
|
||||
let left = rangeStartLeft + (ratioLeft * rangeLeft)
|
||||
let right = rangeStartRight + (ratioRight * rangeRight)
|
||||
let width = right - left
|
||||
if x < firstVariant.left then
|
||||
{ left: firstVariant.left, width: firstVariant.width }
|
||||
else
|
||||
if x >= lastVariant.left + lastVariant.width then do
|
||||
{ left: lastVariant.left, width: lastVariant.width }
|
||||
else do
|
||||
{ left, width }
|
5
src/Yoga/Block/Atom/Toggle.purs
Normal file
5
src/Yoga/Block/Atom/Toggle.purs
Normal file
@ -0,0 +1,5 @@
|
||||
module Yoga.Block.Atom.Toggle
|
||||
( module Yoga.Block.Atom.Toggle.View
|
||||
) where
|
||||
|
||||
import Yoga.Block.Atom.Toggle.View (component, Props)
|
15
src/Yoga/Block/Atom/Toggle/Spec.purs
Normal file
15
src/Yoga/Block/Atom/Toggle/Spec.purs
Normal file
@ -0,0 +1,15 @@
|
||||
module Yoga.Block.Atom.Toggle.Spec where
|
||||
|
||||
import Yoga.Prelude.Spec
|
||||
import Yoga.Block.Atom.Toggle as Toggle
|
||||
|
||||
spec ∷ Spec Unit
|
||||
spec =
|
||||
after_ cleanup do
|
||||
describe "The toggle" do
|
||||
it "renders without errors" do
|
||||
void
|
||||
$ renderComponent Toggle.component
|
||||
{ value: true
|
||||
, onToggle: mempty
|
||||
}
|
71
src/Yoga/Block/Atom/Toggle/Story.purs
Normal file
71
src/Yoga/Block/Atom/Toggle/Story.purs
Normal file
@ -0,0 +1,71 @@
|
||||
module Yoga.Block.Atom.Toggle.Story where
|
||||
|
||||
import Prelude
|
||||
import Color as Color
|
||||
import Data.Maybe (Maybe(..))
|
||||
import Data.Tuple.Nested ((/\))
|
||||
import Effect (Effect)
|
||||
import React.Basic (JSX, element, fragment)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Hooks as React
|
||||
import Yoga.Block as Block
|
||||
import Yoga.Block.Atom.Toggle as Toggle
|
||||
import Yoga.Block.Container.Style (DarkOrLightMode(..))
|
||||
|
||||
default ∷
|
||||
{ title ∷ String
|
||||
}
|
||||
default =
|
||||
{ title: "Atom/Toggle"
|
||||
}
|
||||
|
||||
toggle ∷ Effect JSX
|
||||
toggle = do
|
||||
example <- mkBasicExample
|
||||
darkLight <- mkDarkLightToggle
|
||||
pure
|
||||
$ fragment
|
||||
[ R.div_
|
||||
[ R.h2_ [ R.text "Basics" ]
|
||||
, element example {}
|
||||
, R.h2_ [ R.text "Dark Light toggle" ]
|
||||
, element darkLight {}
|
||||
]
|
||||
]
|
||||
where
|
||||
mkBasicExample =
|
||||
React.reactComponent "Toggle example" \p -> React.do
|
||||
isOn /\ turnOnOrOff <- React.useState' false
|
||||
pure
|
||||
$ element Toggle.component
|
||||
{ value: isOn
|
||||
, onToggle: turnOnOrOff
|
||||
}
|
||||
|
||||
mkDarkLightToggle =
|
||||
React.reactComponent "Toggle dark night example" \p -> React.do
|
||||
isOn /\ turnOnOrOff <- React.useState' false
|
||||
theme /\ setTheme <- React.useState' Nothing
|
||||
let
|
||||
content =
|
||||
element Toggle.component
|
||||
{ value: isOn
|
||||
, onToggle:
|
||||
\b -> do
|
||||
turnOnOrOff b
|
||||
setTheme (Just if b then DarkMode else LightMode)
|
||||
, on: R.text "🌒"
|
||||
, off: R.text "🌞"
|
||||
, backgroundOn:
|
||||
Color.hsl 205.0 1.0 0.93
|
||||
, backgroundOff:
|
||||
Color.hsl 260.0 0.7 0.45
|
||||
}
|
||||
pure
|
||||
$ case theme of
|
||||
Nothing -> element Block.container { content }
|
||||
Just themeVariant ->
|
||||
element Block.container
|
||||
{ content
|
||||
, themeVariant: themeVariant
|
||||
}
|
85
src/Yoga/Block/Atom/Toggle/Style.purs
Normal file
85
src/Yoga/Block/Atom/Toggle/Style.purs
Normal file
@ -0,0 +1,85 @@
|
||||
module Yoga.Block.Atom.Toggle.Style where
|
||||
|
||||
import Yoga.Prelude.Style
|
||||
import Yoga.Block.Container.Style (colour)
|
||||
|
||||
type Props f r =
|
||||
( css ∷ f Style
|
||||
, backgroundOn ∷ f Color
|
||||
, backgroundOff ∷ f Color
|
||||
| r
|
||||
)
|
||||
|
||||
button ∷ Style
|
||||
button =
|
||||
css
|
||||
{ position: relative
|
||||
, background: str colour.inputBackground
|
||||
, border: str $ "1px solid " <> colour.inputBorder
|
||||
, borderRadius: str "calc(var(--s2) / 2)"
|
||||
, height: var "--s2"
|
||||
, width: str "calc(1.15 * var(--s3))"
|
||||
, margin: _0
|
||||
, padding: _0
|
||||
}
|
||||
|
||||
theToggle ∷ ∀ p. { | Props OptionalProp p } -> Style
|
||||
theToggle props =
|
||||
css
|
||||
{ width: var "--s2"
|
||||
, height: var "--s2"
|
||||
, background: str $ colour.interfaceBackground
|
||||
, border: none
|
||||
, borderRadius: str $ "calc(var(--s2) / 2)"
|
||||
, position: absolute
|
||||
, top: str "-1px"
|
||||
, left: _0
|
||||
, margin: _0
|
||||
, boxShadow: str "0 0.5px 3px rgba(0,0,0,0.50)"
|
||||
}
|
||||
|
||||
toggleTextContainer ∷ Style
|
||||
toggleTextContainer =
|
||||
css
|
||||
{ width: _100percent
|
||||
, border: none
|
||||
, fontWeight: str "bold"
|
||||
, position: absolute
|
||||
, top: _0
|
||||
, lineHeight: var "--s2"
|
||||
, height: var "--s2"
|
||||
, display: flex
|
||||
}
|
||||
|
||||
toggleText ∷ Style
|
||||
toggleText =
|
||||
css
|
||||
{ textAlign: str "left"
|
||||
, margin: _0
|
||||
, width: str "50%"
|
||||
, height: str "100%"
|
||||
, display: flex
|
||||
, justifyContent: center
|
||||
, alignItems: center
|
||||
, color: str colour.interfaceTextDisabled
|
||||
, "&:first-child":
|
||||
nest
|
||||
{ color: str colour.successText
|
||||
}
|
||||
}
|
||||
|
||||
inputDisabled ∷ Style
|
||||
inputDisabled =
|
||||
css
|
||||
{ "input[type=toggle]::-webkit-slider-thumb": nested thumbStyleDisabled
|
||||
, "input[type=toggle]::-moz-toggle-thumb": nested thumbStyleDisabled
|
||||
}
|
||||
where
|
||||
thumbStyleDisabled =
|
||||
css
|
||||
{ background: str "#fcfcfc"
|
||||
, boxShadow: str "0 calc(var(--s-4)/2) var(--s-3) rgba(88,88,88,0.2)"
|
||||
}
|
||||
|
||||
disabled ∷ Style
|
||||
disabled = css { backgroundColor: str colour.background30 }
|
203
src/Yoga/Block/Atom/Toggle/View.purs
Normal file
203
src/Yoga/Block/Atom/Toggle/View.purs
Normal file
@ -0,0 +1,203 @@
|
||||
module Yoga.Block.Atom.Toggle.View (component, MandatoryProps, Props, PropsF) where
|
||||
|
||||
import Yoga.Prelude.View
|
||||
import Color as Color
|
||||
import Data.Interpolate (i)
|
||||
import Effect.Class.Console as Console
|
||||
import Foreign.Object as Object
|
||||
import Framer.Motion as Motion
|
||||
import React.Basic.DOM (css)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Emotion as Emotion
|
||||
import React.Basic.Hooks as React
|
||||
import Yoga.Block.Atom.Toggle.Style as Style
|
||||
import Yoga.Block.Container.Style (colour)
|
||||
|
||||
type PropsF f =
|
||||
( className ∷ f String
|
||||
, on ∷ f JSX
|
||||
, off ∷ f JSX
|
||||
| Style.Props f (MandatoryProps InputProps)
|
||||
)
|
||||
|
||||
type MandatoryProps r =
|
||||
( value ∷ Boolean
|
||||
, onToggle ∷ Boolean -> Effect Unit
|
||||
| r
|
||||
)
|
||||
|
||||
data TappingState
|
||||
= TapAllowed
|
||||
| TapNotAllowed
|
||||
|
||||
data DragState
|
||||
= NotDragging
|
||||
| Dragging { startX ∷ Number }
|
||||
| DragDone { startX ∷ Number, endX ∷ Number }
|
||||
|
||||
derive instance eqDragState ∷ Eq DragState
|
||||
|
||||
instance showDragState ∷ Show DragState where
|
||||
show = case _ of
|
||||
NotDragging -> "NotDragging"
|
||||
Dragging x -> "Dragging " <> (show x)
|
||||
DragDone x -> "DragDone " <> (show x)
|
||||
|
||||
type Props =
|
||||
PropsF Id
|
||||
|
||||
type PropsOptional =
|
||||
PropsF OptionalProp
|
||||
|
||||
component ∷ ∀ p p_. Union p p_ Props => ReactComponent { | MandatoryProps p }
|
||||
component = rawComponent
|
||||
|
||||
rawComponent ∷ ∀ p. ReactComponent (Record p)
|
||||
rawComponent =
|
||||
mkForwardRefComponent "Toggle" do
|
||||
\(props ∷ { | PropsOptional }) ref -> React.do
|
||||
let disabled = props.disabled
|
||||
tapState <- useRef TapNotAllowed
|
||||
dragState /\ setDragState <- React.useState' NotDragging
|
||||
maxLeft /\ setMaxLeft <- useState' 0.0
|
||||
buttonRef <- useRef null
|
||||
toggleRef <- useRef null
|
||||
leftState /\ setLeftState <- React.useState' 0.0
|
||||
let
|
||||
getWidth aRef = do
|
||||
bbox <- getBoundingBoxFromRef aRef
|
||||
pure $ bbox <#> _.width # fromMaybe 0.0
|
||||
buttonWidth = getWidth buttonRef
|
||||
toggleWidth = getWidth toggleRef
|
||||
useLayoutEffectAlways do
|
||||
when (maxLeft == 0.0) do
|
||||
bw <- buttonWidth
|
||||
tw <- toggleWidth
|
||||
setMaxLeft (bw - tw)
|
||||
mempty
|
||||
let
|
||||
toggleVariants =
|
||||
{ off: { x: 0.0 }
|
||||
, on: { x: maxLeft }
|
||||
}
|
||||
toggleVariant = Motion.makeVariantLabels toggleVariants
|
||||
buttonVariants =
|
||||
{ off: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOn) ?|| Emotion.str colour.inputBackground }
|
||||
, on: { backgroundColor: (Emotion.str <<< Color.cssStringRGBA <$> props.backgroundOff) ?|| Emotion.str colour.success }
|
||||
}
|
||||
buttonVariant = Motion.makeVariantLabels buttonVariants
|
||||
hasFocus /\ setHasFocus <- useState' false
|
||||
useEffect dragState do
|
||||
case dragState of
|
||||
NotDragging -> mempty
|
||||
Dragging { startX } -> writeRef tapState TapNotAllowed
|
||||
DragDone { startX, endX } -> do
|
||||
maybeBbox <- getBoundingBoxFromRef buttonRef
|
||||
for_ maybeBbox \bbox -> do
|
||||
if endX - startX <= (bbox.left - startX) + (bbox.width / 2.0) then do
|
||||
props.onToggle false
|
||||
else do
|
||||
props.onToggle true
|
||||
mempty
|
||||
pure
|
||||
$ styled Motion.button
|
||||
{ className: "ry-toggle"
|
||||
, css: Style.button <> guard props.disabled Style.inputDisabled
|
||||
, onFocus: handler_ $ setHasFocus true
|
||||
, onBlur: handler_ $ setHasFocus false
|
||||
, transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
|
||||
, variants: Motion.variants buttonVariants
|
||||
, animate: Motion.animate if props.value then buttonVariant.on else buttonVariant.off
|
||||
, value: show props.value
|
||||
, onClick: handler preventDefault \_ -> props.onToggle (not props.value)
|
||||
, style: props.style
|
||||
, _data: Object.singleton "testid" "toggle-testid"
|
||||
, role: "switch"
|
||||
, _aria: Object.singleton "checked" "switch"
|
||||
, ref: buttonRef
|
||||
}
|
||||
[ styled R.div'
|
||||
{ className: "ry-toggle-text"
|
||||
, css: Style.toggleTextContainer
|
||||
}
|
||||
[ styled R.div'
|
||||
{ className: "ry-toggle-text-on"
|
||||
, css: Style.toggleText
|
||||
}
|
||||
[ el Motion.animatePresence {}
|
||||
[ guard (props.value)
|
||||
$ styled Motion.div
|
||||
{ className: "ry-toggle-text-on-container"
|
||||
, css: Style.toggleOnText
|
||||
, key: "ry-toggle-text-on-container"
|
||||
, initial: Motion.initial $ css { scale: 0, opacity: 0 }
|
||||
, animate: Motion.animate $ css { scale: 1, opacity: 1 }
|
||||
, exit: Motion.exit $ css { scale: 0, opacity: 0 }
|
||||
}
|
||||
[ props.on ?|| R.text "I" ]
|
||||
]
|
||||
]
|
||||
, styled R.div'
|
||||
{ className: "ry-toggle-text-off"
|
||||
, css: Style.toggleText
|
||||
}
|
||||
[ el Motion.animatePresence {}
|
||||
[ guard (not props.value)
|
||||
$ styled Motion.div
|
||||
{ className: "ry-toggle-text-on-container"
|
||||
, css: Style.toggleOnText
|
||||
, key: "ry-toggle-text-on-container"
|
||||
, initial: Motion.initial $ css { scale: 0, opacity: 0 }
|
||||
, animate: Motion.animate $ css { scale: 1, opacity: 1 }
|
||||
, exit: Motion.exit $ css { scale: 0, opacity: 0 }
|
||||
}
|
||||
[ props.off ?|| R.text "O" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, styled Motion.div
|
||||
{ className: "ry-toggle-toggle"
|
||||
, css: Style.theToggle props
|
||||
, layout: Motion.layout true
|
||||
, onClick: handler stopPropagation mempty
|
||||
, drag: Motion.drag "x"
|
||||
, dragMomentum: Motion.dragMomentum false
|
||||
, dragElastic: Motion.dragElastic false
|
||||
, dragConstraints:
|
||||
Motion.dragConstraints
|
||||
{ left: if props.value then negate maxLeft else zero
|
||||
, right: if props.value then zero else maxLeft
|
||||
}
|
||||
, variants: Motion.variants toggleVariants
|
||||
, transition: Motion.transition { type: "tween", duration: 0.33, ease: "easeOut" }
|
||||
, whileTap: Motion.prop $ css { scale: 0.85, transition: { type: "tween", duration: 0.10, ease: "easeInOut" } }
|
||||
, onTapStart: Motion.onTapStart \_ _ -> writeRef tapState TapAllowed
|
||||
, onTap:
|
||||
Motion.onTap \_ pi -> do
|
||||
ts <- readRef tapState
|
||||
case ts of
|
||||
TapAllowed -> props.onToggle $ not props.value
|
||||
_ -> mempty
|
||||
mempty
|
||||
, onTapCancel:
|
||||
Motion.onTapCancel \_ pi -> do
|
||||
writeRef tapState TapNotAllowed
|
||||
mempty
|
||||
, animate: Motion.animate if props.value then toggleVariant.on else toggleVariant.off
|
||||
, onDragStart:
|
||||
Motion.onDragStart \_ pi -> do
|
||||
maybeBBox <- getBoundingBoxFromRef toggleRef
|
||||
let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
|
||||
setDragState $ Dragging { startX: x # fromMaybe pi.point.x }
|
||||
, onDragEnd:
|
||||
Motion.onDragEnd \_ pi -> do
|
||||
case dragState of
|
||||
Dragging { startX } -> do
|
||||
maybeBBox <- getBoundingBoxFromRef toggleRef
|
||||
let x = maybeBBox <#> \bbox -> bbox.left + (bbox.width / 2.0)
|
||||
setDragState (DragDone { startX, endX: x # fromMaybe pi.point.x })
|
||||
other -> Console.warn $ i "Unexpected drag state " (show other) " in onDragEvent"
|
||||
, ref: toggleRef
|
||||
}
|
||||
[]
|
||||
]
|
@ -1,18 +1,18 @@
|
||||
module Yoga.Block.Container.Spec where
|
||||
|
||||
import Yoga.Prelude.Spec
|
||||
import Yoga.Block.Container as Container
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Hooks (reactChildrenFromArray)
|
||||
import React.Basic.Hooks (JSX)
|
||||
import Yoga.Block.Container as Container
|
||||
|
||||
spec ∷ Spec Unit
|
||||
spec =
|
||||
after_ cleanup do
|
||||
describe "The container" do
|
||||
it "renders without errors" do
|
||||
void $ renderComponent Container.component { children: reactChildrenFromArray [] }
|
||||
void $ renderComponent Container.component { content: mempty ∷ JSX }
|
||||
it "displays its children" do
|
||||
let children = reactChildrenFromArray [ R.text "Test Text" ]
|
||||
{ findByText } <- renderComponent Container.component { children }
|
||||
let content = R.text "Test Text"
|
||||
{ findByText } <- renderComponent Container.component { content }
|
||||
elem <- findByText "Test Text"
|
||||
elem `textContentShouldEqual` "Test Text"
|
||||
|
@ -1,15 +1,14 @@
|
||||
module Yoga.Block.Container.Story where
|
||||
|
||||
import Prelude
|
||||
import Yoga.Block.Layout.Cluster as Cluster
|
||||
import Effect (Effect)
|
||||
import React.Basic (JSX, element, fragment)
|
||||
import React.Basic.DOM as R
|
||||
import Yoga (el, styledLeaf)
|
||||
import Yoga.Block.Container as Container
|
||||
import Yoga.Block.Container.Style (inputFocus)
|
||||
import Yoga.Block.Layout.Cluster as Cluster
|
||||
import Yoga.Block.Layout.Stack as Stack
|
||||
import Effect (Effect)
|
||||
import React.Basic (JSX, element)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Hooks (reactChildrenFromArray)
|
||||
import Yoga (el, styledLeaf)
|
||||
|
||||
default ∷ { title ∷ String }
|
||||
default = { title: "Pages/Container" }
|
||||
@ -18,8 +17,8 @@ container ∷ Effect JSX
|
||||
container =
|
||||
pure
|
||||
( element Container.component
|
||||
{ children:
|
||||
reactChildrenFromArray
|
||||
{ content:
|
||||
fragment
|
||||
[ R.text "Content"
|
||||
, el Stack.component {}
|
||||
[ el Cluster.component {}
|
||||
|
@ -8,8 +8,27 @@ import Foreign.Object as Object
|
||||
import Heterogeneous.Mapping (class HMapWithIndex, class MappingWithIndex, hmap, hmapWithIndex)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
|
||||
data DarkOrLightMode
|
||||
= DarkMode
|
||||
| LightMode
|
||||
|
||||
lightModeStyle ∷ Style
|
||||
lightModeStyle = unsafeCoerce lightModeVariables
|
||||
|
||||
darkModeStyle ∷ Style
|
||||
darkModeStyle = unsafeCoerce darkModeVariables
|
||||
|
||||
darkMode ∷ Style
|
||||
darkMode = mkGlobal (Just DarkMode)
|
||||
|
||||
lightMode ∷ Style
|
||||
lightMode = mkGlobal (Just LightMode)
|
||||
|
||||
global ∷ Style
|
||||
global =
|
||||
global = mkGlobal Nothing
|
||||
|
||||
mkGlobal ∷ Maybe DarkOrLightMode -> Style
|
||||
mkGlobal maybeMode =
|
||||
css
|
||||
{ "body, html":
|
||||
nested
|
||||
@ -17,7 +36,8 @@ global =
|
||||
{ minHeight: 100.0 # vh
|
||||
, minWidth: 100.0 # vw
|
||||
, lineHeight: str "1.15"
|
||||
, "-webkit-text-size-adjust": _100percent
|
||||
, "WebkitTextSizeAdjust": _100percent
|
||||
, transition: str "background,color 0.33s ease-in"
|
||||
}
|
||||
, ":root":
|
||||
nested $ variables
|
||||
@ -35,7 +55,10 @@ global =
|
||||
, color: str colour.text
|
||||
, margin: str "0"
|
||||
}
|
||||
<> colourTheme defaultColours
|
||||
<> case maybeMode of
|
||||
Nothing -> autoSwitchColourTheme
|
||||
Just DarkMode -> darkModeStyle
|
||||
Just LightMode -> lightModeStyle
|
||||
, "pre,code":
|
||||
nest
|
||||
{ fontFamily: str "var(--monoFont)"
|
||||
@ -81,7 +104,10 @@ defaultColours =
|
||||
, background80: darken 0.8 lightBg
|
||||
, background90: darken 0.9 lightBg
|
||||
, background100: darken 1.0 lightBg
|
||||
, success
|
||||
, successText
|
||||
, interfaceBackground: lightBg
|
||||
, interfaceTextDisabled: darken 0.33 lightBg
|
||||
, interfaceBackgroundHighlight: darken 0.07 lightBg
|
||||
, interfaceBackgroundShadow: darken 0.1 lightBg
|
||||
, inputBackground: darken 0.03 lightBg
|
||||
@ -108,10 +134,13 @@ defaultColours =
|
||||
, background90: lighten 0.9 darkBg
|
||||
, background100: lighten 1.0 darkBg
|
||||
, 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
|
||||
, inputBorder: lighten 0.17 darkBg
|
||||
, success
|
||||
, successText
|
||||
, highlight
|
||||
, text: lightBg
|
||||
}
|
||||
@ -119,6 +148,10 @@ defaultColours =
|
||||
where
|
||||
highlight = Color.rgb 0x00 0x99 0xFF
|
||||
|
||||
success = Color.rgb 20 200 60
|
||||
|
||||
successText = Color.rgb 250 250 250
|
||||
|
||||
-- highlight = Color.rgb 0x10 0x45 0x4A
|
||||
darkBg = Color.rgb 0 0 0
|
||||
|
||||
@ -143,11 +176,14 @@ type FlatTheme a =
|
||||
, background90 ∷ a
|
||||
, background100 ∷ a
|
||||
, interfaceBackground ∷ a
|
||||
, interfaceTextDisabled ∷ a
|
||||
, interfaceBackgroundHighlight ∷ a
|
||||
, interfaceBackgroundShadow ∷ a
|
||||
, inputBackground ∷ a
|
||||
, inputBorder ∷ a
|
||||
, highlight ∷ a
|
||||
, success ∷ a
|
||||
, successText ∷ a
|
||||
, text ∷ a
|
||||
}
|
||||
|
||||
@ -172,8 +208,8 @@ colour =
|
||||
hmap (\x -> "var(" <> x <> ")")
|
||||
$ makeCSSVarLabels defaultColours.light
|
||||
|
||||
colourTheme ∷ Colours -> Style
|
||||
colourTheme { dark, light } = lightT
|
||||
autoSwitchColourTheme ∷ Style
|
||||
autoSwitchColourTheme = lightT
|
||||
where
|
||||
darkT ∷ Style
|
||||
darkT = unsafeCoerce darkObj
|
||||
@ -182,18 +218,30 @@ colourTheme { dark, light } = lightT
|
||||
darkObj =
|
||||
Object.fromHomogeneous defaultColours.dark
|
||||
# Object.foldMap \k v ->
|
||||
Object.singleton ("--" <> k) (str (Color.toHexString v))
|
||||
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
|
||||
|
||||
lightObj ∷ Object StyleProperty
|
||||
lightObj =
|
||||
Object.fromHomogeneous defaultColours.light
|
||||
# Object.foldMap \k v ->
|
||||
Object.singleton ("--" <> k) (str (Color.toHexString v))
|
||||
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
|
||||
# Object.insert "@media (prefers-color-scheme: dark)" (nested darkT)
|
||||
|
||||
lightT ∷ Style
|
||||
lightT = unsafeCoerce lightObj
|
||||
|
||||
lightModeVariables ∷ Object StyleProperty
|
||||
lightModeVariables =
|
||||
Object.fromHomogeneous defaultColours.light
|
||||
# Object.foldMap \k v ->
|
||||
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
|
||||
|
||||
darkModeVariables ∷ Object StyleProperty
|
||||
darkModeVariables =
|
||||
Object.fromHomogeneous defaultColours.dark
|
||||
# Object.foldMap \k v ->
|
||||
Object.singleton ("--" <> k) (str (Color.cssStringRGBA v))
|
||||
|
||||
variables ∷ Style
|
||||
variables =
|
||||
css
|
||||
|
@ -1,22 +1,43 @@
|
||||
module Yoga.Block.Container.View (component, Props) where
|
||||
module Yoga.Block.Container.View
|
||||
( component, Props, PropsF
|
||||
) where
|
||||
|
||||
import Yoga.Prelude.View
|
||||
import Data.Array as Array
|
||||
import Effect.Unsafe (unsafePerformEffect)
|
||||
import React.Basic.DOM as R
|
||||
import React.Basic.Emotion as E
|
||||
import React.Basic.Hooks (reactComponentWithChildren)
|
||||
import React.Basic.Hooks (reactComponent)
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
import Yoga.Block.Container.Style (DarkOrLightMode)
|
||||
import Yoga.Block.Container.Style as Styles
|
||||
|
||||
type Props =
|
||||
{ children ∷ ReactChildren JSX }
|
||||
type PropsF f =
|
||||
( content ∷ JSX
|
||||
, themeVariant ∷ f DarkOrLightMode
|
||||
)
|
||||
|
||||
component ∷ ReactComponent Props
|
||||
component =
|
||||
unsafePerformEffect
|
||||
$ reactComponentWithChildren "Container" \({ children } ∷ Props) -> React.do
|
||||
type Props =
|
||||
( | PropsF Id )
|
||||
|
||||
component ∷ ∀ p q. Union p q Props => ReactComponent { | p }
|
||||
component = rawComponent
|
||||
|
||||
rawComponent ∷ ∀ p. ReactComponent { | p }
|
||||
rawComponent =
|
||||
unsafeCoerce
|
||||
$ unsafePerformEffect
|
||||
$ reactComponent "Container" \({ content, themeVariant } ∷ { | PropsF OptionalProp }) -> React.do
|
||||
pure
|
||||
$ R.div_
|
||||
$ Array.cons
|
||||
(element E.global { styles: Styles.global })
|
||||
(reactChildrenToArray children)
|
||||
( element E.global
|
||||
{ styles:
|
||||
case opToMaybe themeVariant of
|
||||
Nothing -> Styles.global
|
||||
Just Styles.DarkMode -> Styles.darkMode
|
||||
Just Styles.LightMode -> Styles.lightMode
|
||||
}
|
||||
)
|
||||
[ content
|
||||
]
|
||||
|
@ -1,17 +1,27 @@
|
||||
module Yoga.Prelude.Default
|
||||
( module Prelude
|
||||
, module Control.Alt
|
||||
, module Control.Monad.Maybe.Trans
|
||||
, module Control.Monad.Trans.Class
|
||||
, module Data.Maybe
|
||||
, module Data.Either
|
||||
, module Effect
|
||||
, module Effect.Class
|
||||
, module Data.Monoid
|
||||
, module Data.Foldable
|
||||
, module Data.FoldableWithIndex
|
||||
, module Data.FunctorWithIndex
|
||||
) where
|
||||
|
||||
import Prelude
|
||||
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.Maybe (Maybe(..), fromMaybe)
|
||||
import Data.FoldableWithIndex (foldMapWithIndex)
|
||||
import Data.FunctorWithIndex (mapWithIndex)
|
||||
import Data.Maybe (Maybe(..), fromMaybe, fromMaybe', isJust, maybe)
|
||||
import Data.Monoid (guard)
|
||||
import Effect (Effect)
|
||||
import Effect.Class (liftEffect)
|
||||
|
@ -9,21 +9,34 @@ module Yoga.Prelude.View
|
||||
, module React.Basic.DOM.Events
|
||||
, module Type.Row
|
||||
, module Web.DOM
|
||||
, module Web.HTML.HTMLElement
|
||||
, module Data.Nullable
|
||||
, module Untagged.Castable
|
||||
, NodeRef
|
||||
, getBoundingBoxFromRef
|
||||
) where
|
||||
|
||||
import Yoga.Prelude.Default
|
||||
import Data.Nullable (Nullable, notNull, null)
|
||||
import Prim.Row (class Union)
|
||||
import React.Basic.DOM (Props_div)
|
||||
import React.Basic.DOM.Events (preventDefault, targetValue)
|
||||
import React.Basic.DOM.Events (stopPropagation, preventDefault, targetValue)
|
||||
import React.Basic.Events (class Merge, EventFn, EventHandler, SyntheticEvent, handler, handler_, merge, mergeImpl, syntheticEvent, unsafeEventFn)
|
||||
import React.Basic.Hooks (type (/\), Component, Hook, JSX, Pure, ReactChildren, ReactComponent, ReactContext, Ref, Render, UnsafeReference(..), UseContext, UseDebugValue, UseEffect, UseLayoutEffect, UseMemo, UseReducer, UseRef, UseState, coerceHook, consumer, contextConsumer, contextProvider, createContext, displayName, element, elementKeyed, empty, fragment, keyed, memo, provider, reactChildrenFromArray, reactChildrenToArray, reactComponentFromHook, readRef, readRefMaybe, unsafeHook, unsafeRenderEffect, useContext, useDebugValue, useEffect, useEffectAlways, useEffectOnce, useLayoutEffect, useLayoutEffectAlways, useLayoutEffectOnce, useMemo, useReducer, useRef, useState, useState', writeRef, (/\))
|
||||
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
|
||||
|
@ -1160,6 +1160,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.5.4.tgz#de25b5da9f727985a3757fd59b5d028aba75841a"
|
||||
integrity sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==
|
||||
|
||||
"@popperjs/core@^2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
|
||||
integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
|
||||
|
||||
"@reach/router@^1.3.3":
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c"
|
||||
|
Loading…
Reference in New Issue
Block a user