From 774db186ef64fdd9264d26c2f6146657ed0a4987 Mon Sep 17 00:00:00 2001 From: Mark Eibes Date: Thu, 17 Dec 2020 22:53:10 +0100 Subject: [PATCH] Add popper --- package.json | 4 +- src/Data/TwoOrMore.purs | 2 +- src/Framer/Motion.js | 1 + src/Framer/Motion.purs | 83 +++++- src/Framer/MotionValue.js | 3 +- src/Framer/MotionValue.purs | 11 +- src/React/Basic/Popper/Hook.js | 3 + src/React/Basic/Popper/Hook.purs | 64 +++++ src/React/Basic/Popper/Story.purs | 205 +++++++++++++++ src/Yoga.purs | 1 + src/Yoga/Block.purs | 4 +- src/Yoga/Block/Atom/Range/Style.purs | 1 - src/Yoga/Block/Atom/Segmented.purs | 2 +- src/Yoga/Block/Atom/Segmented/Style.purs | 25 +- src/Yoga/Block/Atom/Segmented/View.purs | 242 +++--------------- .../Atom/Segmented/View/ActiveIndicator.purs | 185 +++++++++++++ src/Yoga/Block/Atom/Toggle.purs | 5 + src/Yoga/Block/Atom/Toggle/Spec.purs | 15 ++ src/Yoga/Block/Atom/Toggle/Story.purs | 71 +++++ src/Yoga/Block/Atom/Toggle/Style.purs | 85 ++++++ src/Yoga/Block/Atom/Toggle/View.purs | 203 +++++++++++++++ src/Yoga/Block/Container/Spec.purs | 10 +- src/Yoga/Block/Container/Story.purs | 15 +- src/Yoga/Block/Container/Style.purs | 62 ++++- src/Yoga/Block/Container/View.purs | 41 ++- src/Yoga/Prelude/Default.purs | 12 +- src/Yoga/Prelude/View.purs | 15 +- yarn.lock | 5 + 28 files changed, 1107 insertions(+), 268 deletions(-) create mode 100644 src/React/Basic/Popper/Hook.js create mode 100644 src/React/Basic/Popper/Hook.purs create mode 100644 src/React/Basic/Popper/Story.purs create mode 100644 src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs create mode 100644 src/Yoga/Block/Atom/Toggle.purs create mode 100644 src/Yoga/Block/Atom/Toggle/Spec.purs create mode 100644 src/Yoga/Block/Atom/Toggle/Story.purs create mode 100644 src/Yoga/Block/Atom/Toggle/Style.purs create mode 100644 src/Yoga/Block/Atom/Toggle/View.purs diff --git a/package.json b/package.json index 1f96964..0070d23 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/Data/TwoOrMore.purs b/src/Data/TwoOrMore.purs index 58df70c..52b4d03 100644 --- a/src/Data/TwoOrMore.purs +++ b/src/Data/TwoOrMore.purs @@ -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 diff --git a/src/Framer/Motion.js b/src/Framer/Motion.js index e93b39b..94c6fb0 100644 --- a/src/Framer/Motion.js +++ b/src/Framer/Motion.js @@ -1,6 +1,7 @@ const framerMotion = require("framer-motion") exports.divImpl = framerMotion.motion.div +exports.buttonImpl = framerMotion.motion.button exports.h1Impl = framerMotion.motion.h1 exports.svgImpl = framerMotion.motion.svg exports.pathImpl = framerMotion.motion.path diff --git a/src/Framer/Motion.purs b/src/Framer/Motion.purs index 5970f03..30273f0 100644 --- a/src/Framer/Motion.purs +++ b/src/Framer/Motion.purs @@ -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 diff --git a/src/Framer/MotionValue.js b/src/Framer/MotionValue.js index 348638d..93884f9 100644 --- a/src/Framer/MotionValue.js +++ b/src/Framer/MotionValue.js @@ -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() } \ No newline at end of file +exports.stop = mv => () => { return mv.stop() } +exports.onChangeImpl = callback => mv => { return () => mv.onChange(callback); } \ No newline at end of file diff --git a/src/Framer/MotionValue.purs b/src/Framer/MotionValue.purs index 9f1e90e..8487318 100644 --- a/src/Framer/MotionValue.purs +++ b/src/Framer/MotionValue.purs @@ -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 = diff --git a/src/React/Basic/Popper/Hook.js b/src/React/Basic/Popper/Hook.js new file mode 100644 index 0000000..0b0785c --- /dev/null +++ b/src/React/Basic/Popper/Hook.js @@ -0,0 +1,3 @@ +const reactPopper = require('react-popper') + +exports.usePopperImpl = reactPopper.usePopper \ No newline at end of file diff --git a/src/React/Basic/Popper/Hook.purs b/src/React/Basic/Popper/Hook.purs new file mode 100644 index 0000000..965d98b --- /dev/null +++ b/src/React/Basic/Popper/Hook.purs @@ -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 + } + } diff --git a/src/React/Basic/Popper/Story.purs b/src/React/Basic/Popper/Story.purs new file mode 100644 index 0000000..7e17012 --- /dev/null +++ b/src/React/Basic/Popper/Story.purs @@ -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 + } + ] + ] + ] diff --git a/src/Yoga.purs b/src/Yoga.purs index ee31657..7ef4bdf 100644 --- a/src/Yoga.purs +++ b/src/Yoga.purs @@ -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) diff --git a/src/Yoga/Block.purs b/src/Yoga/Block.purs index 9890eb1..2b91d9a 100644 --- a/src/Yoga/Block.purs +++ b/src/Yoga/Block.purs @@ -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 } diff --git a/src/Yoga/Block/Atom/Range/Style.purs b/src/Yoga/Block/Atom/Range/Style.purs index 2baa0d1..c669adf 100644 --- a/src/Yoga/Block/Atom/Range/Style.purs +++ b/src/Yoga/Block/Atom/Range/Style.purs @@ -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 = diff --git a/src/Yoga/Block/Atom/Segmented.purs b/src/Yoga/Block/Atom/Segmented.purs index 866fce5..a78f7c8 100644 --- a/src/Yoga/Block/Atom/Segmented.purs +++ b/src/Yoga/Block/Atom/Segmented.purs @@ -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) diff --git a/src/Yoga/Block/Atom/Segmented/Style.purs b/src/Yoga/Block/Atom/Segmented/Style.purs index 0d29df6..4da8e54 100644 --- a/src/Yoga/Block/Atom/Segmented/Style.purs +++ b/src/Yoga/Block/Atom/Segmented/Style.purs @@ -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)" diff --git a/src/Yoga/Block/Atom/Segmented/View.purs b/src/Yoga/Block/Atom/Segmented/View.purs index 44222fe..f43e7ef 100644 --- a/src/Yoga/Block/Atom/Segmented/View.purs +++ b/src/Yoga/Block/Atom/Segmented/View.purs @@ -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 diff --git a/src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs b/src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs new file mode 100644 index 0000000..90ff1e7 --- /dev/null +++ b/src/Yoga/Block/Atom/Segmented/View/ActiveIndicator.purs @@ -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 } diff --git a/src/Yoga/Block/Atom/Toggle.purs b/src/Yoga/Block/Atom/Toggle.purs new file mode 100644 index 0000000..f2f9913 --- /dev/null +++ b/src/Yoga/Block/Atom/Toggle.purs @@ -0,0 +1,5 @@ +module Yoga.Block.Atom.Toggle + ( module Yoga.Block.Atom.Toggle.View + ) where + +import Yoga.Block.Atom.Toggle.View (component, Props) diff --git a/src/Yoga/Block/Atom/Toggle/Spec.purs b/src/Yoga/Block/Atom/Toggle/Spec.purs new file mode 100644 index 0000000..1187545 --- /dev/null +++ b/src/Yoga/Block/Atom/Toggle/Spec.purs @@ -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 + } diff --git a/src/Yoga/Block/Atom/Toggle/Story.purs b/src/Yoga/Block/Atom/Toggle/Story.purs new file mode 100644 index 0000000..9c8f062 --- /dev/null +++ b/src/Yoga/Block/Atom/Toggle/Story.purs @@ -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 + } diff --git a/src/Yoga/Block/Atom/Toggle/Style.purs b/src/Yoga/Block/Atom/Toggle/Style.purs new file mode 100644 index 0000000..9b1e861 --- /dev/null +++ b/src/Yoga/Block/Atom/Toggle/Style.purs @@ -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 } diff --git a/src/Yoga/Block/Atom/Toggle/View.purs b/src/Yoga/Block/Atom/Toggle/View.purs new file mode 100644 index 0000000..56b291d --- /dev/null +++ b/src/Yoga/Block/Atom/Toggle/View.purs @@ -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 + } + [] + ] diff --git a/src/Yoga/Block/Container/Spec.purs b/src/Yoga/Block/Container/Spec.purs index cc15d99..178a683 100644 --- a/src/Yoga/Block/Container/Spec.purs +++ b/src/Yoga/Block/Container/Spec.purs @@ -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" diff --git a/src/Yoga/Block/Container/Story.purs b/src/Yoga/Block/Container/Story.purs index 6ca4d2b..fd2459e 100644 --- a/src/Yoga/Block/Container/Story.purs +++ b/src/Yoga/Block/Container/Story.purs @@ -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 {} diff --git a/src/Yoga/Block/Container/Style.purs b/src/Yoga/Block/Container/Style.purs index 7018813..b18289b 100644 --- a/src/Yoga/Block/Container/Style.purs +++ b/src/Yoga/Block/Container/Style.purs @@ -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 diff --git a/src/Yoga/Block/Container/View.purs b/src/Yoga/Block/Container/View.purs index 8ee2ee3..e28fe3a 100644 --- a/src/Yoga/Block/Container/View.purs +++ b/src/Yoga/Block/Container/View.purs @@ -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 + ] diff --git a/src/Yoga/Prelude/Default.purs b/src/Yoga/Prelude/Default.purs index 212cd41..d76de60 100644 --- a/src/Yoga/Prelude/Default.purs +++ b/src/Yoga/Prelude/Default.purs @@ -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) diff --git a/src/Yoga/Prelude/View.purs b/src/Yoga/Prelude/View.purs index 1521f58..6bdad6e 100644 --- a/src/Yoga/Prelude/View.purs +++ b/src/Yoga/Prelude/View.purs @@ -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 diff --git a/yarn.lock b/yarn.lock index e20f50d..b819422 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"