diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c33954f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist-newstyle/ diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..156f2b3 --- /dev/null +++ b/default.nix @@ -0,0 +1,14 @@ +{ mkDerivation, base, ClasshSS, containers, data-default, filepath +, lens, lib, reflex-classhss, reflex-dom-core, text +}: +mkDerivation { + pname = "templates"; + version = "0.1.0.0"; + src = ./.; + libraryHaskellDepends = [ + base ClasshSS containers data-default filepath lens reflex-classhss + reflex-dom-core text + ]; + description = "Ace templates library"; + license = lib.licenses.bsd3; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..810d9cc --- /dev/null +++ b/shell.nix @@ -0,0 +1,25 @@ +{ nixpkgs ? import {}, compiler ? "default", doBenchmark ? false }: + +let + inherit (nixpkgs) pkgs; + haskellPackages = if compiler == "default" + then pkgs.haskellPackages + else pkgs.haskell.packages.${compiler}; + + variant = if doBenchmark then pkgs.haskell.lib.doBenchmark else pkgs.lib.id; + + # Override haskellPackages to use local versions + haskellPackages' = haskellPackages.override { + overrides = self: super: { + ClasshSS = self.callCabal2nix "ClasshSS" ../ClasshSS-dev {}; + reflex-classhss = self.callCabal2nix "reflex-classhss" ../reflex-classh {}; + }; + }; + + templates = import ./default.nix; + drv = variant (haskellPackages'.callPackage templates {}); +in +pkgs.mkShell { + buildInputs = [ pkgs.cabal-install ]; + inputsFrom = [ (if pkgs.lib.inNixShell then drv.env else drv) ]; +} diff --git a/src/Templates/Partials/Buttons.hs b/src/Templates/Partials/Buttons.hs index 046f814..f37f27d 100644 --- a/src/Templates/Partials/Buttons.hs +++ b/src/Templates/Partials/Buttons.hs @@ -7,82 +7,215 @@ import Classh as C import Classh.Reflex as C import Data.Text as T import Reflex.Dom.Core -import Control.Lens ((%~)) +import Control.Lens ((%~), (.~)) import Data.Proxy import Data.Char (isAlphaNum, isSpace) -iconButton :: DomBuilder t m => Text -> m (Event t ()) -iconButton icon = do - (e, _) <- elClass' "button" classes $ text icon + +-- sendButton :: DomBuilder t m => m (Event t ()) +-- sendButton = iconButton "send" + +iconButton' + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color (with states) + -> C.WhenTW C.ColorWithOpacity -- ^ Text/icon color + -> Text -- ^ Icon name + -> m (Event t ()) +iconButton' bgCol txtCol icon = do + (e, _) <- elClass' "button" classes $ C.textS textCfg icon pure $ domEvent Click e where - classes = "focus:outline-none bg-primary rounded p-2.5 w-[50%] font-icon text-icon text-white leading-none shadow-button h-[95%]" - -iconButton' :: (DomBuilder t m, PostBuild t m) => Dynamic t Bool -> Text -> m (Event t ()) -iconButton' enabled icon = do - let attrs = ffor enabled $ \b -> if b then "class" =: classes else "class" =: classes <> "disabled" =: "1" - (e, _) <- elDynAttr' "button" attrs $ text icon + textCfg = C.classhUnsafe + [ C.text_color .~ txtCol + , C.custom .~ "font-icon text-icon leading-none" + ] + classes = C.classhUnsafe + [ C.bgColor .~ bgCol + , C.br .~~ C.R_Normal + , C.p .~~ C.TWSize 2.5 + , C.w .~~ C.pct 50 + , C.h .~~ C.pct 95 + , C.shadow .~~ C.Shadow_Md + , C.border . C.outline .~ [("focus", C.Outline_None)] + ] + +iconButtonEnabled' + :: (DomBuilder t m, PostBuild t m) + => C.GradientColor -- ^ Background color + -> C.ColorWithOpacity -- ^ Icon color + -> Dynamic t Bool + -> Text + -> m (Event t ()) +iconButtonEnabled' bgCol iconCol enabled icon = do + let attrs = ffor enabled $ \b -> + if b + then "class" =: classes + else "class" =: classes <> "disabled" =: "1" + (e, _) <- elDynAttr' "button" attrs $ C.textS textCfg icon pure $ domEvent Click e where - classes = "focus:outline-none flex-shrink-0 bg-primary rounded p-2.5 font-icon text-icon text-white leading-none shadow-button" - - - -primaryButton' :: (DomBuilder t m, PostBuild t m) => Dynamic t Bool -> Text -> m (Event t ()) -primaryButton' enabled buttonText = do + textCfg = C.classhUnsafe + [ C.text_color .~~ iconCol + , C.custom .~ "font-icon text-icon leading-none" + ] + classes = C.classhUnsafe + [ C.bgColor .~~ bgCol + , C.br .~~ C.R_Normal + , C.p .~~ C.TWSize 2.5 + , C.shadow .~~ C.Shadow_Md + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.custom .~ "flex-shrink-0" + ] + + + +-- primaryButtonDyn :: (DomBuilder t m, PostBuild t m) => Dynamic t Bool -> Text -> m (Event t ()) +-- primaryButtonDyn = primaryButtonDyn' +-- (C.only (C.noTransition (C.solidColor (C.Violet C.C600)))) -- bg +-- [("hover", C.solidColor (C.Violet C.C700) `C.withTransition` C.Duration_200) +-- ,("active", C.solidColor (C.Violet C.C500) `C.withTransition` C.Duration_100)] -- bg states +-- (C.only (C.color C.White)) -- text +-- C.Shadow_Md -- shadow +-- [("focus", C.noTransition C.Ring_4)] -- ring +-- (C.color (C.Violet C.C500)) -- ring color + +primaryButtonDyn' + :: (DomBuilder t m, PostBuild t m) + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.WhenTW C.ColorWithOpacity -- ^ Text color + -> C.BoxShadow -- ^ Shadow + -> [(C.TWCondition, C.WithTransition C.RingWidth)] -- ^ Ring width states + -> C.ColorWithOpacity -- ^ Ring color + -> Dynamic t Bool + -> Text + -> m (Event t ()) +primaryButtonDyn' bgCol bgStates txtCol shadowVal ringStates ringCol enabled buttonText = do let attrs = ffor enabled $ \b -> if b then "class" =: classes else "class" =: classes <> "disabled" =: "1" - (e, _) <- elDynAttr' "button" attrs $ text buttonText + (e, _) <- elDynAttr' "button" attrs $ C.textS textCfg buttonText pure $ domEvent Click e where - classes = - "focus:outline-none w-full p-4 mt-12 shadow-button bg-primary \ - \ font-facit font-bold text-white text-body text-center rounded \ - \ hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50" - -secondaryIconButton :: DomBuilder t m => Text -> Text -> m (Event t ()) -secondaryIconButton cs icon = do + textCfg = C.classhUnsafe + [ C.text_color .~ txtCol + , C.text_weight .~~ C.Bold + , C.custom .~ "font-facit text-body text-center" + ] + classes = C.classhUnsafe + [ C.bgColor .~ bgCol + , C.bgColor .~^ bgStates + , C.w .~~ C.TWSize_Full + , C.p .~~ C.TWSize 4 + , C.mt .~~ C.twSize' 12 + , C.shadow .~~ shadowVal + , C.br .~~ C.R_Normal + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ ringStates + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + ] + +secondaryIconButton + :: DomBuilder t m + => C.GradientColor -- ^ Background color + -> C.ColorWithOpacity -- ^ Icon color + -> C.ColorWithOpacity -- ^ Border color + -> C.ColorWithOpacity -- ^ Ring color + -> Text -- ^ Extra classes + -> Text -- ^ Icon name + -> m (Event t ()) +secondaryIconButton bgCol iconCol borderCol ringCol cs icon = do (e, _) <- elClass' "button" classes $ - elClass "div" "font-icon leading-none text-icon text-primary-dark" $ text icon + elClass "div" iconClasses $ C.textS iconTextCfg icon pure $ domEvent Click e where - classes = - "focus:outline-none rounded border border-metaline \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ p-2.5 flex-shrink-0 bg-primary-light " <> cs - - -secondaryButton :: DomBuilder t m => Text -> Text -> m (Event t ()) -secondaryButton cs label = do - (e, _) <- elClass' "button" classes $ - text label + iconTextCfg = C.classhUnsafe [ C.text_color .~~ iconCol ] + iconClasses = C.classhUnsafe [ C.box_custom .~ "font-icon leading-none text-icon" ] + classes = C.classhUnsafe + [ C.bgColor .~~ bgCol + , C.bc .~~ borderCol + , C.bw .~~ C.B1 + , C.br .~~ C.R_Normal + , C.p .~~ C.TWSize 2.5 + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.custom .~ ("flex-shrink-0 " <> cs) + ] + + +secondaryButton + :: DomBuilder t m + => C.GradientColor -- ^ Background color + -> C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Border color + -> C.ColorWithOpacity -- ^ Ring color + -> Text -- ^ Extra classes + -> Text -- ^ Label + -> m (Event t ()) +secondaryButton bgCol txtCol borderCol ringCol cs label = do + (e, _) <- elClass' "button" classes $ C.textS textCfg label pure $ domEvent Click e where - classes = - "w-full p-2.5 leading-none text-center rounded border border-metaline \ - \ bg-primary-light text-primary-darker font-bold font-facit focus:outline-none \ - \ focus:ring-4 ring-primary ring-opacity-50 " <> cs - -sendButton :: DomBuilder t m => m (Event t ()) -sendButton = iconButton "send" - - -navyBlueButton :: DomBuilder t m => Text -> m (Event t ()) -navyBlueButton buttonText = do - (e, _) <- elClass' "button" other $ C.textS textCfg' buttonText + textCfg = C.classhUnsafe + [ C.text_color .~~ txtCol + , C.text_weight .~~ C.Bold + , C.custom .~ "font-facit leading-none text-center" + ] + classes = C.classhUnsafe + [ C.bgColor .~~ bgCol + , C.bc .~~ borderCol + , C.bw .~~ C.B1 + , C.br .~~ C.R_Normal + , C.w .~~ C.TWSize_Full + , C.p .~~ C.TWSize 2.5 + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.custom .~ cs + ] + + +-- navyBlueButton :: DomBuilder t m => Text -> m (Event t ()) +-- navyBlueButton = navyBlueButton' +-- (C.only (C.noTransition (C.solidColor (C.Violet C.C600)))) -- violet for visibility on dark bg +-- [("hover", C.solidColor (C.Violet C.C500) `C.withTransition` C.Duration_200) +-- ,("active", C.solidColor (C.Violet C.C400) `C.withTransition` C.Duration_100)] +-- (C.only (C.color C.White)) +-- (C.color (C.Violet C.C300)) -- ring color + +navyBlueButton' + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.WhenTW C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Ring color + -> Text + -> m (Event t ()) +navyBlueButton' bgCol bgStates txtCol ringCol buttonText = do + (e, _) <- elClass' "button" boxCfg $ C.textS textCfg buttonText pure $ domEvent Click e where - textCfg' = $(C.classh' [ C.text_color C..~~ C.White - , C.text_font C..~~ C.Font_Custom "Sarabun" - , C.text_weight C..~~ C.Bold - ]) - other = - "focus:outline-none w-full p-4 shadow-button bg-[#2E3A59] \ - \ rounded-3xl hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ transition-all duration-300 ease-in-out \ - \ transform hover:scale-105 active:scale-95 \ - \ hover:shadow-md active:shadow-lg" + textCfg = C.classhUnsafe [ C.text_color .~ txtCol + , C.text_font .~~ C.Font_Custom "Sarabun" + , C.text_weight .~~ C.Bold + ] + boxCfg = C.classhUnsafe [ C.w .~~ C.TWSize_Full + , C.p .~~ C.TWSize 4 + , C.bgColor .~ bgCol + , C.bgColor .~^ bgStates + , C.br .~~ C.R_3Xl + , C.shadow .~^ [("def", C.noTransition C.Shadow_Md) + , ("hover", C.Shadow_Md `C.withTransition` C.Duration_300) + ] + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.transform . C.scale .~^ [("hover", C.Scale_105 `C.withTransition` C.Duration_200) + ,("active", C.Scale_95 `C.withTransition` C.Duration_100)] + ] @@ -93,89 +226,180 @@ primaryButtonImageDyn :: ( PostBuild t m , DomBuilder t m ) - => Dynamic t Text + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Ring color + -> Dynamic t Text -> Height -> Width -> m (Event t ()) -primaryButtonImageDyn dynImageHref height width = do - (e, _) <- elClass' "button" classes $ - elDynAttr "img" imgAttrs blank +primaryButtonImageDyn bgCol bgStates ringCol dynImageHref height width = do + (e, _) <- elClass' "button" boxCfg $ + elDynAttr "img" imgAttrs blank pure $ domEvent Click e where otherImgAttrs = "height" =: height <> "width" =: width <> "class" =: "block mx-auto" imgAttrs = (\src -> otherImgAttrs <> "src" =: src) <$> dynImageHref - classes = - "focus:outline-none w-full p-4 mt-16 shadow-button bg-[#00B9DA] \ - \ font-[Sarabun] font-bold text-white text-body text-center rounded-xl \ - \ hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ transition-all duration-300 ease-in-out \ - \ transform hover:scale-105 active:scale-95 \ - \ hover:shadow-md active:shadow-lg" + boxCfg = C.classhUnsafe [ C.w .~~ C.TWSize_Full + , C.p .~~ C.TWSize 4 + , C.mt .~~ C.twSize' 16 + , C.bgColor .~ bgCol + , C.bgColor .~^ bgStates + , C.br .~~ C.R_Xl + , C.shadow .~^ [("def", C.noTransition C.Shadow_Md) + , ("hover", C.Shadow_Md `C.withTransition` C.Duration_300) + ] + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.transform . C.scale .~^ [("hover", C.Scale_105 `C.withTransition` C.Duration_200) + ,("active", C.Scale_95 `C.withTransition` C.Duration_100)] + , C.custom .~ "font-[Sarabun] font-bold text-white text-body text-center" + ] type Height = Text type Width = Text -primaryButtonImage :: DomBuilder t m => Text -> Height -> Width -> m (Event t ()) -primaryButtonImage imageHref height width = do - (e, _) <- elClass' "button" classes $ +-- primaryButtonImage :: DomBuilder t m => Text -> Height -> Width -> m (Event t ()) +-- primaryButtonImage = primaryButtonImage' +-- (C.only (C.noTransition (C.solidColor (C.Cyan C.C500)))) +-- [("hover", C.solidColor (C.Cyan C.C600) `C.withTransition` C.Duration_200) +-- ,("active", C.solidColor (C.Cyan C.C400) `C.withTransition` C.Duration_100)] +-- (C.color (C.Cyan C.C500)) + +primaryButtonImage' + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Ring color + -> Text -> Height -> Width -> m (Event t ()) +primaryButtonImage' bgCol bgStates ringCol imageHref height width = do + (e, _) <- elClass' "button" (primaryButtonBoxCfg bgCol bgStates ringCol) $ elAttr "img" ("src" =: imageHref <> "height" =: height <> "width" =: width <> "class" =: "block mx-auto") blank pure $ domEvent Click e - where - classes = - "focus:outline-none w-full p-4 mt-16 shadow-button bg-[#00B9DA] \ - \ font-[Sarabun] font-bold text-white text-body text-center rounded-xl \ - \ hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ transition-all duration-300 ease-in-out \ - \ transform hover:scale-105 active:scale-95 \ - \ hover:shadow-md active:shadow-lg" - -primaryButtonImageText :: DomBuilder t m => Text -> Height -> Width -> Text -> m (Event t ()) -primaryButtonImageText imageHref height width bottomText = do - (e, _) <- elClass' "button" classes $ do + +primaryButtonImageText + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Ring color + -> Text -> Height -> Width -> Text -> m (Event t ()) +primaryButtonImageText bgCol bgStates txtCol ringCol imageHref height width bottomText = do + (e, _) <- elClass' "button" (primaryButtonBoxCfg bgCol bgStates ringCol) $ do elAttr "img" ("src" =: imageHref <> "height" =: height <> "width" =: width <> "class" =: "block mx-auto") blank - text bottomText -- TODO: replace with styledText + C.textS textCfg bottomText pure $ domEvent Click e where - classes = - "focus:outline-none w-full p-4 mt-16 shadow-button bg-[#00B9DA] \ - \ font-[Sarabun] font-bold text-white text-body text-center rounded-xl \ - \ hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ transition-all duration-300 ease-in-out \ - \ transform hover:scale-105 active:scale-95 \ - \ hover:shadow-md active:shadow-lg" - - -primaryButtonSized :: DomBuilder t m => TWSize -> TWSize -> Text -> m (Event t ()) -primaryButtonSized height width buttonText = do - (e, _) <- elAttr' "button" ("class" =: classTW <> "name" =: name) $ text buttonText + textCfg = C.classhUnsafe [ C.text_color .~~ txtCol + , C.text_font .~~ C.Font_Custom "Sarabun" + , C.text_weight .~~ C.Bold + ] + +primaryButtonBoxCfg + :: C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Ring color + -> Text +primaryButtonBoxCfg bgCol bgStates ringCol = C.classhUnsafe + [ C.w .~~ C.TWSize_Full + , C.p .~~ C.TWSize 4 + , C.mt .~~ C.twSize' 16 + , C.bgColor .~ bgCol + , C.bgColor .~^ bgStates + , C.br .~~ C.R_Xl + , C.shadow .~^ [("def", C.noTransition C.Shadow_Md) + , ("hover", C.Shadow_Md `C.withTransition` C.Duration_300) + ] + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.transform . C.scale .~^ [("hover", C.Scale_105 `C.withTransition` C.Duration_200) + ,("active", C.Scale_95 `C.withTransition` C.Duration_100)] + , C.custom .~ "text-center" + ] + + +-- primaryButtonSized :: DomBuilder t m => TWSize -> TWSize -> Text -> m (Event t ()) +-- primaryButtonSized = primaryButtonSized' +-- (C.only (C.noTransition (C.solidColor (C.Cyan C.C500)))) +-- [("hover", C.solidColor (C.Cyan C.C600) `C.withTransition` C.Duration_200) +-- ,("active", C.solidColor (C.Cyan C.C400) `C.withTransition` C.Duration_100)] +-- (C.color C.White) +-- (C.color (C.Cyan C.C500)) + +primaryButtonSized' + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Ring color + -> TWSize -> TWSize -> Text -> m (Event t ()) +primaryButtonSized' bgCol bgStates txtCol ringCol height width buttonText = do + (e, _) <- elAttr' "button" ("class" =: classCfg <> "name" =: name) $ + C.textS textCfg buttonText pure $ domEvent Click e where - classTW = primaryClass <&> ("py-" <> showTW height) <&> ("px-" <> showTW width) - -- for testing / selenium mainly + textCfg = C.classhUnsafe [ C.text_color .~~ txtCol + , C.text_font .~~ C.Font_Custom "Sarabun" + , C.text_weight .~~ C.Bold + , C.text_size .|~ [C.XS, C.XS, C.LG] -- text-xs on mobile, text-lg on md+ + ] + classCfg = primaryClass bgCol bgStates ringCol <&> C.classhUnsafe [C.py .~~ height, C.px .~~ width] name = T.filter (\c -> isAlphaNum c || isSpace c ) buttonText -primaryButton :: DomBuilder t m => Text -> m (Event t ()) -primaryButton buttonText = do - (e, _) <- elAttr' "button" ("class" =: (primaryClass <&> "py-4 px-8") <> "name" =: name) $ text buttonText +-- primaryButton :: DomBuilder t m => Text -> m (Event t ()) +-- primaryButton = primaryButton' +-- (C.only (C.noTransition (C.solidColor (C.Cyan C.C500)))) +-- [("hover", C.solidColor (C.Cyan C.C600) `C.withTransition` C.Duration_200) +-- ,("active", C.solidColor (C.Cyan C.C400) `C.withTransition` C.Duration_100)] +-- (C.color C.White) +-- (C.color (C.Cyan C.C500)) + +primaryButton' + :: DomBuilder t m + => C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Ring color + -> Text -> m (Event t ()) +primaryButton' bgCol bgStates txtCol ringCol buttonText = do + (e, _) <- elAttr' "button" ("class" =: classCfg <> "name" =: name) $ + C.textS textCfg buttonText pure $ domEvent Click e where - -- for testing / selenium mainly + textCfg = C.classhUnsafe [ C.text_color .~~ txtCol + , C.text_font .~~ C.Font_Custom "Sarabun" + , C.text_weight .~~ C.Bold + , C.text_size .|~ [C.XS, C.XS, C.LG] -- text-xs on mobile, text-lg on md+ + ] + classCfg = primaryClass bgCol bgStates ringCol <&> C.classhUnsafe [C.py .~~ C.TWSize 4, C.px .~~ C.TWSize 8] name = T.filter (\c -> isAlphaNum c || isSpace c ) buttonText -primaryClass = - "focus:outline-none shadow-button bg-[#00B9DA] \ - \ font-[Sarabun] font-bold text-white text-body text-center rounded-xl \ - \ hover:bg-primary-rich active:bg-primary-desaturated \ - \ focus:ring-4 ring-primary ring-opacity-50 \ - \ transition-all duration-300 ease-in-out \ - \ transform hover:scale-105 active:scale-95 \ - \ whitespace-nowrap inline-block \ - \ hover:shadow-md active:shadow-lg min-[0px]:text-xs md:text-lg" +primaryClass + :: C.WhenTW (C.WithTransition C.GradientColor) -- ^ Background color + -> [(C.TWCondition, C.WithTransition C.GradientColor)] -- ^ Background hover/active states + -> C.ColorWithOpacity -- ^ Ring color + -> Text +primaryClass bgCol bgStates ringCol = C.classhUnsafe + [ C.bgColor .~ bgCol + , C.bgColor .~^ bgStates + , C.br .~~ C.R_Xl + , C.shadow .~^ [("def", C.noTransition C.Shadow_Md) + , ("hover", C.Shadow_Md `C.withTransition` C.Duration_300) + ] + , C.border . C.outline .~ [("focus", C.Outline_None)] + , C.border . C.ring . C.ringWidth .~ [("focus", C.noTransition C.Ring_4)] + , C.border . C.ring . C.ringColor .~~ ringCol + , C.border . C.ring . C.ringOpacity .~~ 50 + , C.transform . C.scale .~^ [("hover", C.Scale_105 `C.withTransition` C.Duration_200) + ,("active", C.Scale_95 `C.withTransition` C.Duration_100)] + , C.custom .~ "text-center whitespace-nowrap inline-block" + ] example :: Template t m => m (Event t ()) example = buttonToggleBody "" True $ \case diff --git a/src/Templates/Partials/Containers.hs b/src/Templates/Partials/Containers.hs index ad9c8ce..86340f0 100644 --- a/src/Templates/Partials/Containers.hs +++ b/src/Templates/Partials/Containers.hs @@ -13,17 +13,32 @@ import Control.Monad.Fix import Control.Monad import Reflex.Dom.Core +-- | Initial state for collapsible containers +data CollapsibleState = Open | Closed + deriving (Eq, Show) + +isOpen :: CollapsibleState -> Bool +isOpen Open = True +isOpen Closed = False + screenContainer :: (DomBuilder t m) => m a -> m a screenContainer = elClass "div" $(classh' [w .~~ TWSize_Screen, h .~~ TWSize_Screen, custom .~ "flex flex-col overflow-hidden"]) -toggleButton :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => Text -> m (Dynamic t Bool) -toggleButton label = do +-- toggleButton :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => Text -> m (Dynamic t Bool) +-- toggleButton = toggleButton' (color White) + +-- | Parameterized version with custom text color +toggleButton' :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => ColorWithOpacity -> Text -> m (Dynamic t Bool) +toggleButton' txtCol label = do let - classes :: Bool -> Text - classes shown = - "text-icon font-icon transform select-none " -- Icon display classes - <> "transition-transform duration-300 ease-in-out " -- Animation settings classes - <> bool "rotate-180" mempty shown -- Do a little twirl! + boxClasses :: Bool -> Text + boxClasses shown = classhUnsafe $ + [ custom .~ "text-icon font-icon select-none" ] + <> (if shown + then [ transform . rotate .~^ [("def", Rotate_0 `withTransition` Duration_300 `withTiming` Ease_InOut)] ] + else [ transform . rotate .~^ [("def", Rotate_180 `withTransition` Duration_300 `withTiming` Ease_InOut)] ] + ) + textStyle = classhUnsafe [ text_color .~~ txtCol ] -- The icon for a rendered container is a triangle pointing up, and -- the icon for an unrendered container is that same triangle, rotated @@ -31,11 +46,12 @@ toggleButton label = do -- be animated; Having the "neutral" position be "no transform" means -- the icon won't do a twirl on page load. rec - (labelEl, _) <- elClass' "div" $(classh' [ mt .~~ TWSize 8 - , custom .~ "cursor-pointer flex flex-row justify-between" - ]) $ do - textS $(classh' [ text_color .~~ White ]) label - elDynClass' "span" (classes <$> toggled) $ text "expand_less" + (labelEl, _) <- elClass' "div" (classhUnsafe [ mt .~~ twSize' 8 + , cursor .~~ CursorPointer + , custom .~ "flex flex-row justify-between" + ]) $ do + textS (classhUnsafe [ text_color .~~ txtCol ]) label + elDynClass "span" (boxClasses <$> toggled) $ textS textStyle "expand_less" let toggleEv = domEvent Click labelEl toggled <- holdUniqDyn =<< toggle True toggleEv @@ -47,21 +63,27 @@ toggleButton label = do -- | Wrap the body contents in a container that can be collapsed by -- clicking on its header. As an optimisation, when the container is -- collapsed, the inner tree is not rendered. -collapsibleContainer_ :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => Text -> m a -> m () -collapsibleContainer_ label body = do - toggled <- toggleButton label +collapsibleContainer_ + :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) + => ColorWithOpacity -- ^ Text color + -> Text -> m a -> m () +collapsibleContainer_ txtCol label body = do + toggled <- toggleButton' txtCol label dyn_ (bool blank (void body) <$> toggled) -- | Wrap the body contents in a container that can be collapsed by -- clicking on its header. -collapsibleContainer :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => Text -> m a -> m a -collapsibleContainer label body = do +collapsibleContainer + :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) + => ColorWithOpacity -- ^ Text color + -> Text -> m a -> m a +collapsibleContainer txtCol label body = do let bodyClasses :: Bool -> Text bodyClasses shown = bool "hidden" mempty shown - toggled <- toggleButton label + toggled <- toggleButton' txtCol label elDynClass "div" (bodyClasses <$> toggled) body @@ -70,27 +92,30 @@ collapsibleContainer label body = do -- clicking on its header. collapsibleContainerWithImage :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) - => Text + => CollapsibleState -- ^ Initial state + -> Text -> Text -> m a -> m a -collapsibleContainerWithImage imgSrc label body = do +collapsibleContainerWithImage initialState imgSrc label body = do let bodyClasses :: Bool -> Text bodyClasses shown = bool "hidden" mempty shown - toggled <- toggleButton' imgSrc label + toggled <- toggleButtonWithImage initialState imgSrc label elDynClass "div" (bodyClasses <$> toggled) body -toggleButton' :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => Text -> Text -> m (Dynamic t Bool) -toggleButton' imgSrc label = do +toggleButtonWithImage :: (MonadFix m, DomBuilder t m, PostBuild t m, MonadHold t m) => CollapsibleState -> Text -> Text -> m (Dynamic t Bool) +toggleButtonWithImage initialState imgSrc label = do let classes :: Bool -> Text - classes shown = - "text-icon font-icon transform select-none " -- Icon display classes - <> "transition-transform duration-300 ease-in-out " -- Animation settings classes - <> bool "rotate-180" mempty shown -- Do a little twirl! + classes shown = classhUnsafe $ + [ custom .~ "text-icon font-icon select-none" ] + <> (if shown + then [ transform . rotate .~^ [("def", Rotate_0 `withTransition` Duration_300 `withTiming` Ease_InOut)] ] + else [ transform . rotate .~^ [("def", Rotate_180 `withTransition` Duration_300 `withTiming` Ease_InOut)] ] + ) -- The icon for a rendered container is a triangle pointing up, and -- the icon for an unrendered container is that same triangle, rotated @@ -105,21 +130,21 @@ toggleButton' imgSrc label = do dynText "expand_less" let toggleEv = domEvent Click labelEl - toggled <- holdUniqDyn =<< toggle True toggleEv + toggled <- holdUniqDyn =<< toggle (isOpen initialState) toggleEv pure toggled -openCloseButton :: Template t m => ImgSrc -> Color -> T.Text -> m (Event t ()) +openCloseButton :: Template t m => ImgSrc -> ColorWithOpacity -> T.Text -> m (Event t ()) openCloseButton imgSrc tColor name = do buttonToggleBody (constDyn "pl-10") True $ \case True -> do --gridCol Col12 $ do - col [6] $ imgClass imgSrc $(classh' [custom .~ "rotate-180 inline-block", position .~~ centered ]) + col [6] $ imgClass imgSrc $(classh' [transform . rotate .~^ [("def", noTransition Rotate_180)], custom .~ "inline-block", position .~~ centered ]) divClass $(classh' [colSpan .|~ [6], position .~~ centered, custom .~ "inline-block"]) $ do textS (classhUnsafe [text_color .~~ tColor]) $ "close" <&> name False -> do - col [6] $ imgClass imgSrc $(classh' [custom .~ "rotate-180 inline-block", position .~~ centered ]) + col [6] $ imgClass imgSrc $(classh' [transform . rotate .~^ [("def", noTransition Rotate_180)], custom .~ "inline-block", position .~~ centered ]) divClass $(classh' [colSpan .|~ [6], position .~~ centered, custom .~ "inline-block"]) $ do textS (classhUnsafe [text_color .~~ tColor]) $ "open" <&> name diff --git a/src/Templates/Partials/Containers/Dropdown.hs b/src/Templates/Partials/Containers/Dropdown.hs index d7e0287..4ee8c10 100644 --- a/src/Templates/Partials/Containers/Dropdown.hs +++ b/src/Templates/Partials/Containers/Dropdown.hs @@ -8,21 +8,50 @@ import Reflex.Dom.Core import Control.Monad.Fix import Data.Maybe import qualified Data.Map as Map +import Data.Map ((!)) import qualified Data.Text as T -- | TODO: take an element config for the options elements as well -- | TODO: clean up and upstream to reflex-dom-contrib -dropdown' +-- dropdown' +-- :: ( MonadFix m +-- , DomBuilder t m +-- ) +-- => Map.Map a T.Text +-- -> SelectElementConfig er t (DomBuilderSpace m) +-- -> m (Dynamic t a) +-- dropdown' = dropdown'' +-- (only (noTransition (solidColor White))) +-- [ ("def", noTransition (color (Gray C300))) +-- , ("focus", noTransition (color (Cyan C500))) +-- ] + +-- | Parameterized version with custom colors +dropdown'' :: ( MonadFix m , DomBuilder t m ) - => Map.Map a T.Text + => WhenTW (WithTransition GradientColor) -- ^ Background color + -> WhenTW (WithTransition ColorWithOpacity) -- ^ Border color (including focus state) + -> Map.Map a T.Text -> SelectElementConfig er t (DomBuilderSpace m) -> m (Dynamic t a) -dropdown' options cfg' = mdo - let class' = "w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:border-[#00B9DA] font-[Sarabun] text-lg mb-5 bg-white" - let safeInitial = (snd . head $ Map.toList options) +dropdown'' bgCol borderCol options cfg' = mdo + let class' = classhUnsafe [ w .~~ TWSize_Full + , px .~~ TWSize 4 + , py .~~ TWSize 3 + , bw .~~ B1 + , bc .~ borderCol + , br .~~ R_Lg + , border . outline .~ [("focus", Outline_None)] + , mb .~~ twSize' 5 + , bgColor .~ bgCol + , custom .~ "font-[Sarabun] text-lg" + ] + let safeInitial = case Map.toList options of + [] -> error "dropdown' requires non-empty options map" + (k,v):_ -> v let cfg = cfg' & selectElementConfig_initialValue .~ safeInitial & selectElementConfig_setValue .~ optionsEvent @@ -38,24 +67,42 @@ dropdown' options cfg' = mdo (e, _) <- elAttr' "option" ("value" =: optText) $ text optText pure $ optText <$ domEvent Click e -dropdownWithDefault +-- dropdownWithDefault +-- :: ( MonadFix m +-- , DomBuilder t m +-- ) +-- => Map.Map a T.Text +-- -> T.Text +-- -> SelectElementConfig er t (DomBuilderSpace m) +-- -> m (Dynamic t a) +-- dropdownWithDefault = dropdownWithDefault' +-- (only (noTransition (solidColor White))) +-- [ ("def", noTransition (color (Gray C300))) +-- , ("focus", noTransition (color (Cyan C500))) +-- ] + +-- | Parameterized version with custom colors +dropdownWithDefault' :: ( MonadFix m , DomBuilder t m ) - => Map.Map a T.Text + => WhenTW (WithTransition GradientColor) -- ^ Background color + -> WhenTW (WithTransition ColorWithOpacity) -- ^ Border color (including focus state) + -> Map.Map a T.Text -> T.Text -> SelectElementConfig er t (DomBuilderSpace m) -> m (Dynamic t a) -dropdownWithDefault options start cfg' = mdo - let class' = $(classh' [ w .~~ TWSize_Full, px .~~ TWSize 4, py .~~ TWSize 3 - , bw .~~ B1 - , bc .~^ [("def", noTransition (Gray C300)), ("focus", noTransition (hex "00B9DA"))] - , br .~~ R_Lg - , border . bStyle .~ [("focus",BNone)] - , custom .~ "focus:outline-none focus:border-" - , mb .~~ TWSize 5 - , bgColor .~~ White - ]) +dropdownWithDefault' bgCol borderCol options start cfg' = mdo + let class' = classhUnsafe [ w .~~ TWSize_Full + , px .~~ TWSize 4 + , py .~~ TWSize 3 + , bw .~~ B1 + , bc .~ borderCol + , br .~~ R_Lg + , border . outline .~ [("focus", Outline_None)] + , mb .~~ twSize' 5 + , bgColor .~ bgCol + ] let safeInitial = start let cfg = cfg' & selectElementConfig_initialValue .~ safeInitial @@ -69,5 +116,5 @@ dropdownWithDefault options start cfg' = mdo where flipTup (a_,b_) = (b_,a_) makeOpt optText = do - (e, _) <- elAttr' "option" ("value" =: optText) $ textS $(classh' [text_font .~~ Font_Custom "Sarabun", text_size .~~ LG]) optText + (e, _) <- elAttr' "option" ("value" =: optText) $ textS (classhUnsafe [text_font .~~ Font_Custom "Sarabun", text_size .~~ LG]) optText pure $ optText <$ domEvent Click e diff --git a/src/Templates/Partials/Errors.hs b/src/Templates/Partials/Errors.hs index 09ebfe7..843222f 100644 --- a/src/Templates/Partials/Errors.hs +++ b/src/Templates/Partials/Errors.hs @@ -33,7 +33,12 @@ maybeDisplay template val = dyn_ $ ffor val $ \case displayOn :: Template t m => (a -> m ()) -> Event t a -> m () displayOn template ev = maybeDisplay template =<< holdDyn Nothing (Just <$> ev) --- | Render an error message. -errorMessage :: Template t m => Text -> m () -errorMessage t = - elClass "div" $(classh' [mt .~~ TWSize 1, h .~~ twSize' 4]) $ textS "text-opacity-70" t +-- -- | Render an error message. +-- errorMessage :: Template t m => Text -> m () +-- errorMessage = errorMessage' (withOpacity (Rose C500) 70) + +-- | Render an error message with custom text color. +errorMessage' :: Template t m => ColorWithOpacity -> Text -> m () +errorMessage' txtCol t = + elClass "div" $(classh' [mt .~~ twSize' 1, h .~~ twSize' 4]) $ + textS (classhUnsafe [text_color .~~ txtCol]) t diff --git a/src/Templates/Partials/Inputs.hs b/src/Templates/Partials/Inputs.hs index a40b5c1..e676485 100644 --- a/src/Templates/Partials/Inputs.hs +++ b/src/Templates/Partials/Inputs.hs @@ -9,35 +9,37 @@ import Data.Map as Map import Classh as C commonClassesBox :: T.Text -commonClassesBox = $(classh' [mx .~~ TWSize 1, custom .~ "focus:outline-none bg-inset flex-grow" ]) +commonClassesBox = $(classh' [mx .~~ twSize' 1, custom .~ "focus:outline-none bg-inset flex-grow" ]) commonClassesText :: T.Text commonClassesText = $(classh' [text_size .|~ [XL2, XL4], custom .~ "font-label" ]) messageInput' :: Template t m - => Event t (Map AttributeName Text) + => T.Text + -> Event t (Map AttributeName Text) -> Event t () -> m (InputEl t m) -messageInput' newAttributes clearEvent = inputElement $ def - & initialAttributes .~ messageInputAttrs +messageInput' textClass newAttributes clearEvent = inputElement $ def + & initialAttributes .~ messageInputAttrs textClass & inputElementConfig_setValue .~ ("" <$ clearEvent) & (inputElementConfig_elementConfig . elementConfig_modifyAttributes) .~ ((fmap.fmap) Just newAttributes) messageInput :: Template t m - => Event t (Map AttributeName Text) + => T.Text + -> Event t (Map AttributeName Text) -> Event t () -> m (TextAreaElement EventResult (DomBuilderSpace m) t) -messageInput newAttributes clearEvent = textAreaElement $ def - & initialAttributes .~ messageInputAttrs +messageInput textClass newAttributes clearEvent = textAreaElement $ def + & initialAttributes .~ messageInputAttrs textClass & textAreaElementConfig_setValue .~ ("" <$ clearEvent) & (textAreaElementConfig_elementConfig . elementConfig_modifyAttributes) .~ ((fmap.fmap) Just newAttributes) -messageInputAttrs :: Map.Map AttributeName T.Text -messageInputAttrs = - "class" =: $(classh' [w .~~ TWSize_Full, p .~~ TWSize 4, custom .~ "text-2xl"] ) +messageInputAttrs :: T.Text -> Map.Map AttributeName T.Text +messageInputAttrs textClass = + "class" =: ($(classh' [custom .~ "focus:outline-none", bgColor .~~ solidColor (hex "2D2644"), br .~~ R_Lg, w .~~ TWSize_Full, p .~~ TWSize 4]) <> " " <> textClass) <> "placeholder" =: "Type your message" <> "type" =: "text" <> "rows" =: "2" diff --git a/src/Templates/Partials/Invitebar.hs b/src/Templates/Partials/Invitebar.hs index 489ada2..c352b22 100644 --- a/src/Templates/Partials/Invitebar.hs +++ b/src/Templates/Partials/Invitebar.hs @@ -19,43 +19,57 @@ import Templates.Partials.Buttons emailParse :: a -> Either T.Text Bool emailParse _ = Right True --- | Builds an input bar for emails, it returns both the input and --- the button that sends it. -invitebar :: (PostBuild t m, DomBuilder t m, MonadHold t m, MonadFix m) => Text -> m (InputEl t m, Event t ()) -invitebar placeholder = do +-- -- | Builds an input bar for emails, it returns both the input and +-- -- the button that sends it. +-- invitebar :: (PostBuild t m, DomBuilder t m, MonadHold t m, MonadFix m) => Text -> m (InputEl t m, Event t ()) +-- invitebar = invitebar' +-- (only (noTransition (solidColor White))) +-- (only (noTransition (solidColor Transparent))) +-- (only (noTransition (color (Rose C600)))) +-- (only (color (Rose C600))) + +-- | Parameterized version with custom colors +invitebar' + :: (PostBuild t m, DomBuilder t m, MonadHold t m, MonadFix m) + => WhenTW (WithTransition GradientColor) -- ^ Container background color + -> WhenTW (WithTransition GradientColor) -- ^ Input background color + -> WhenTW (WithTransition ColorWithOpacity) -- ^ Border color + -> WhenTW ColorWithOpacity -- ^ Feedback text color + -> GradientColor -- ^ Button background color + -> ColorWithOpacity -- ^ Button icon color + -> Text + -> m (InputEl t m, Event t ()) +invitebar' bgCol inputBgCol borderCol textCol btnBgCol btnIconCol placeholder = do rec - let emailClass e = ffor (emailParse <$> e) $ \case - Right True -> "border-rose-600 text-rose-600" - _ -> "border-rose-600 text-rose-600" - emailText e = case emailParse e of + let emailText e = case emailParse e of Right True -> "Invitation sent to " <> e -- TODO: remove Right False -> "Email must be @aceinterviewprep.io" Left _ -> "Invalid email format" - invbar@(invInput, invButton) <- elClass "div" $(classh' [my .~~ TWSize 2 + invbar@(invInput, invButton) <- elClass "div" (classhUnsafe [my .~~ twSize' 2 , w .~~ TWSize_Full , custom .~ "shadow-button flex flex-row" - , bgColor .~~ White + , bgColor .~ bgCol , br .~~ R_Normal ]) $ do elClass "button" $(classh' [ pl .~~ TWSize 2 ]) $ textS "font-icon text-icon" "email" invInput' <- inputElement $ def & initialAttributes .~ ("class" =: - $(classh' [ w .~~ TWSize_Full - , h .~~ TWSize_Full - , bgColor .~~ Transparent - , px .~~ TWSize 1 - , pt .~~ TWSize 3 - , pb .~~ TWSize 3 - , pr .~~ TWSize 2 - , custom .~ "focus:outline-none flex-grow placeholder-light font-label" - ] - ) + (classhUnsafe [ w .~~ TWSize_Full + , h .~~ TWSize_Full + , bgColor .~ inputBgCol + , px .~~ TWSize 1 + , pt .~~ TWSize 3 + , pb .~~ TWSize 3 + , pr .~~ TWSize 2 + , custom .~ "focus:outline-none flex-grow placeholder-light font-label" + ] + ) <> "placeholder" =: placeholder <> "type" =: "email" ) - invButton' <- iconButton' toEnable "send" + invButton' <- iconButtonEnabled' btnBgCol btnIconCol toEnable "send" return (invInput', invButton') @@ -75,12 +89,18 @@ invitebar placeholder = do invButton let - dynClass = - fmap ( (<>) $(classh' [pl .~~ TWSize 1, pt .~~ TWSize 0, pb .~~ TWSize 1, pr .~~ TWSize 3, custom .~ "text-center"])) - $ toHide - <> (emailClass $ _inputElement_value invInput) + boxClass = classhUnsafe [ pl .~~ TWSize 1 + , pt .~~ TWSize 0 + , pb .~~ TWSize 1 + , pr .~~ TWSize 3 + , custom .~ "text-center" + ] + textClass = classhUnsafe [text_color .~ textCol] + baseClass = boxClass <> " " <> textClass + dynClass = fmap (<> baseClass) toHide elDynClass "div" dynClass $ dynText feedback return invbar + diff --git a/src/Templates/Partials/Lists.hs b/src/Templates/Partials/Lists.hs index e6f50ad..ed0979d 100644 --- a/src/Templates/Partials/Lists.hs +++ b/src/Templates/Partials/Lists.hs @@ -13,27 +13,51 @@ import qualified Data.Text as T import Reflex.Dom.Core -- | Configuration for a list item. +-- All color fields use WhenTW for full user control over states and transitions. data ListItemConfig t = ListItemConfig { _listItemConfig_clickable :: Dynamic t Bool , _listItemConfig_subtext :: Dynamic t (Maybe Text) , _listItemConfig_icon :: Dynamic t (Maybe Text) , _listItemConfig_highlight :: Dynamic t (Maybe Text) , _listItemConfig_unread :: Dynamic t (Maybe Int) + , _listItemConfig_bgColor :: WhenTW (WithTransition GradientColor) + , _listItemConfig_borderColor :: WhenTW (WithTransition ColorWithOpacity) + , _listItemConfig_textColor :: WhenTW ColorWithOpacity + , _listItemConfig_subtextColor :: WhenTW ColorWithOpacity } -- | The default configuration for list items is to have no subtext, no -- icon, no highlighting information, and to not be clickable. -defListItemConfig :: Applicative (Dynamic t) => ListItemConfig t -defListItemConfig = ListItemConfig +-- Uses default light theme colors (for dark backgrounds, use defListItemConfig' with custom colors). +-- defListItemConfig :: Applicative (Dynamic t) => ListItemConfig t +-- defListItemConfig = defListItemConfig' +-- (only (noTransition (solidColor Transparent))) -- bg: transparent +-- (only (noTransition (color (Gray C200)))) -- border: gray-200 +-- (only (color White)) -- text: white +-- (only (color (Gray C300))) + +-- | Configurable version with custom colors +defListItemConfig' + :: Applicative (Dynamic t) + => WhenTW (WithTransition GradientColor) -- ^ Background color (user controls states/transitions) + -> WhenTW (WithTransition ColorWithOpacity) -- ^ Border color + -> WhenTW ColorWithOpacity -- ^ Text color + -> WhenTW ColorWithOpacity -- ^ Subtext color + -> ListItemConfig t +defListItemConfig' bgCol borderCol textCol subtextCol = ListItemConfig { _listItemConfig_clickable = pure False , _listItemConfig_subtext = pure Nothing , _listItemConfig_icon = pure Nothing , _listItemConfig_highlight = pure Nothing - , _listItemConfig_unread = pure Nothing + , _listItemConfig_unread = pure Nothing + , _listItemConfig_bgColor = bgCol + , _listItemConfig_borderColor = borderCol + , _listItemConfig_textColor = textCol + , _listItemConfig_subtextColor = subtextCol } -instance Reflex t => Default (ListItemConfig t) where - def = defListItemConfig +-- instance Reflex t => Default (ListItemConfig t) where +-- def = defListItemConfig -- | Render the given 'Text' with highlighting given by the -- 'ListItemConfig'. @@ -53,27 +77,29 @@ listItem ) => ListItemConfig t -- ^ Visual configuration for the list item -> Dynamic t Text -- ^ The label - -> m (Event t ()) + -> m (Event t ()) -- ^ If the configuration has '_listItemConfig_clickable' set, then -- this is the 'Click' event for the list item. Otherwise, it's -- 'never'. listItem cfg label = do - let - topClass :: (Bool, Maybe Int) -> Text - topClass (click, unread) = T.intercalate " " $ - [ "flex flex-col py-2 border-b border-metaline" ] - <> case click of - True -> ["cursor-pointer hover:bg-[#2E3A59]"] - False -> [] - <> case unread of - Just _ -> [ "bg-[#FF6D31]" ] - Nothing -> [] - let topClassDyn = topClass <$> ((,) <$> _listItemConfig_clickable cfg <*> _listItemConfig_unread cfg) - - + let + topClass :: Bool -> Text + topClass click = classhUnsafe $ + [ py .~~ TWSize 2 + , border . bStyle .~~ BSolid + , bw_b .~~ B1 + , bc_b .~ _listItemConfig_borderColor cfg + , bgColor .~ _listItemConfig_bgColor cfg + ] + <> if click then [ cursor .~~ CursorPointer ] else [] + let topClassDyn = topClass <$> _listItemConfig_clickable cfg + (e, _) <- elDynClass' "div" topClassDyn $ do - elClass "div" "leading-none font-facit text-body text-xl text-white flex flex-row items-center gap-2 overflow-hidden" $ do + let labelClass = classhUnsafe [ custom .~ "leading-none font-facit text-body text-xl flex flex-row items-center gap-2 overflow-hidden" + , text_color .~ _listItemConfig_textColor cfg + ] + elClass "div" labelClass $ do dyn_ $ ffor (_listItemConfig_icon cfg) $ \case Nothing -> blank Just icon -> elClass "span" "font-icon text-icon select-none" $ text icon @@ -81,10 +107,12 @@ listItem cfg label = do el "span" $ dyn_ $ renderHighlight cfg <$> label dyn_ $ ffor (_listItemConfig_subtext cfg) $ \case - Just subtext -> elClass "div" "mt-1 leading-none font-facit text-label text-light" $ text subtext + Just subtext -> textS (classhUnsafe [ custom .~ "mt-1 leading-none font-facit text-label" + , text_color .~ _listItemConfig_subtextColor cfg + ]) subtext Nothing -> blank - + pure $ domEvent Click e diff --git a/src/Templates/Partials/Modal.hs b/src/Templates/Partials/Modal.hs index 45d78ab..4a60ab6 100644 --- a/src/Templates/Partials/Modal.hs +++ b/src/Templates/Partials/Modal.hs @@ -2,6 +2,8 @@ module Templates.Partials.Modal where +import Classh as C +import Classh.Reflex as C import Templates.Partials.Image import Templates.Types import Reflex.Dom.Core @@ -9,25 +11,64 @@ import Control.Monad.Fix import Data.Text as T -modal :: ( DomBuilder t m - , MonadFix m - , MonadHold t m - , PostBuild t m - ) - => ImgSrc +-- modal :: ( DomBuilder t m +-- , MonadFix m +-- , MonadHold t m +-- , PostBuild t m +-- ) +-- => ImgSrc +-- -> Event t () +-- -> m a +-- -> m (Event t a) +-- modal = modal' +-- (C.solidColorOpacity C.Black 40) -- overlay background +-- (C.solidColor (C.Slate C.C900)) -- content background (dark navy) +-- (C.color C.Black) -- border color +-- (C.color C.White) -- text color +-- (C.color (C.Rose C.C500)) -- close button color + +modal' + :: ( DomBuilder t m + , MonadFix m + , MonadHold t m + , PostBuild t m + ) + => C.GradientColor -- ^ Overlay background color (with opacity) + -> C.GradientColor -- ^ Content background color + -> C.ColorWithOpacity -- ^ Border color + -> C.ColorWithOpacity -- ^ Text color + -> C.ColorWithOpacity -- ^ Close button icon color + -> ImgSrc -> Event t () -> m a -> m (Event t a) -modal xButtonImgSrc open modalDom = mdo - let styleBase = "position:fixed;z-index:20;padding-top:100px;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4);" - hideModal = ("style" =: ("display:none;" <> styleBase)) - showModal = ("style" =: ("display:block;" <> styleBase)) +modal' overlayBg contentBg borderCol txtCol closeBtnCol xButtonImgSrc open modalDom = mdo + -- Overlay positioning must remain as inline style (no Classh for position:fixed, z-index, etc.) + let styleBase = "position:fixed;z-index:9999;padding-top:100px;left:0;top:0;width:100%;height:100%;overflow:auto;" + hideModal = ("style" =: ("display:none;visibility:hidden;" <> styleBase) <> "class" =: overlayClass) + showModal = ("style" =: ("display:block;visibility:visible;" <> styleBase) <> "class" =: overlayClass) modalAttrs <- holdDyn hideModal $ mergeWith const [showModal <$ open, hideModal <$ close] close <- elDynAttr "div" modalAttrs $ do - elAttr "div" ("style" =: "background-color:#00004D;margin:auto;padding:20px;width:80%;color:white;" - <> "class" =: "border-double rounded-md border-8 border-black text-base") $ do - e <- fmap fst $ elClass' "span" "pl-5 ml-5 pb-4 text-rose-500 grid justify-items-end" $ do + elAttr "div" ("style" =: "margin:auto;width:80%;" <> "class" =: contentClass) $ do + e <- fmap fst $ elClass' "span" closeSpanClass $ do imgAttr xButtonImgSrc ("height" =: "30px" <> "width" =: "30px") x' <- modalDom pure $ x' <$ (domEvent Click e) pure close + where + overlayClass = C.classhUnsafe [ C.bgColor .~~ overlayBg ] + contentClass = C.classhUnsafe + [ C.bgColor .~~ contentBg + , C.p .~~ C.TWSize 5 -- padding:20px ≈ p-5 + , C.border . C.bStyle .~~ C.BDouble + , C.br .~~ C.R_Md + , C.bw .~~ C.B8 + , C.bc .~~ borderCol + , C.custom .~ "text-base" -- text-base for font sizing + ] <> " " <> C.classhUnsafe [ C.text_color .~~ txtCol ] + closeSpanClass = C.classhUnsafe + [ C.pl .~~ C.TWSize 5 + , C.ml .~~ C.twSize' 5 + , C.pb .~~ C.TWSize 4 + , C.custom .~ "grid justify-items-end" + ] <> " " <> C.classhUnsafe [ C.text_color .~~ closeBtnCol ] diff --git a/src/Templates/Partials/Searchbar.hs b/src/Templates/Partials/Searchbar.hs index cfb3fb0..05b850e 100644 --- a/src/Templates/Partials/Searchbar.hs +++ b/src/Templates/Partials/Searchbar.hs @@ -11,35 +11,50 @@ import Data.Text (Text) import Reflex.Dom.Core import Templates.Types -searchbar +-- searchbar +-- :: DomBuilder t m +-- => Text +-- -> Event t a +-- -> m (InputEl t m) +-- searchbar = searchbar' +-- (only (noTransition (solidColor White))) +-- (only (noTransition (solidColor Transparent))) +-- (only (color Black)) + +-- | Parameterized version with custom colors +searchbar' :: DomBuilder t m - => Text + => WhenTW (WithTransition GradientColor) -- ^ Container background color + -> WhenTW (WithTransition GradientColor) -- ^ Input background color + -> WhenTW ColorWithOpacity -- ^ Text color + -> Text -> Event t a -> m (InputEl t m) -searchbar placeholder clearEvent = do - elClass "div" $(classh' [mt .~~ TWSize 0, w .~~ TWSize_Full, bgColor .~~ White, br .~~ R_Normal, custom .~ "flex flex-row"]) $ do - elClass "button" $(classh' [ px .~~ TWSize 3 - , br .~~ R_Normal - , custom .~ "leading-none shadow-button focus:outline-none font-icon"]) $ text "search" +searchbar' bgCol inputBgCol txtCol placeholder clearEvent = do + elClass "div" (classhUnsafe [mt .~~ twSize' 0, w .~~ TWSize_Full, bgColor .~ bgCol, br .~~ R_Normal, custom .~ "flex flex-row"]) $ do + elClass "button" (classhUnsafe [ px .~~ TWSize 3 + , br .~~ R_Normal + , shadow .~~ Shadow_Md + , border . outline .~ [("focus", Outline_None)] + , custom .~ "leading-none font-icon" + ]) $ text "search" + let inputClass = classhUnsafe [ w .~~ (pix 96) + , h .~~ TWSize_Full + , bgColor .~ inputBgCol + , pl .~~ TWSize 2 + , py .~~ TWSize 1 + , pr .~~ TWSize 3 + , border . outline .~ [("focus", Outline_None)] + , custom .~ "flex-grow placeholder-light" + ] + <> classhUnsafe [ text_color .~ txtCol + , text_weight .~~ Light + , text_size .~~ XL + , custom .~ "text-icon" + ] inputElement $ def - & initialAttributes .~ - ("class" =: ($(classh' [ w .~~ (pix 96) - , h .~~ TWSize_Full - , bgColor .~~ Transparent - , pl .~~ TWSize 2 - , py .~~ TWSize 1 - , pr .~~ TWSize 3 - , custom .~ "focus:outline-none flex-grow placeholder-light" - ] - ) <> $(classh' [ text_color .~~ Black - , text_weight .~~ Light - , text_size .~~ XL - , custom .~ "text-icon" - ] - ) - ) - <> "placeholder" =: placeholder - <> "type" =: "text" - ) + & initialAttributes .~ ("class" =: inputClass + <> "placeholder" =: placeholder + <> "type" =: "text") & inputElementConfig_setValue .~ (mempty <$ clearEvent) diff --git a/src/Templates/Partials/Shapes.hs b/src/Templates/Partials/Shapes.hs index 3ce558f..8501ced 100644 --- a/src/Templates/Partials/Shapes.hs +++ b/src/Templates/Partials/Shapes.hs @@ -6,44 +6,28 @@ import Classh import Classh.Reflex import Templates.Types import Reflex.Dom.Core -import qualified Data.Text as T --- | Simple circle with specified radius and color -circle :: DomBuilder t m => T.Text -> Color -> m () -circle rad c = circle' (parseRadius rad) c - where - -- Parse "10px" to 5.0 (radius is half diameter) - parseRadius r = case T.stripSuffix "px" r of - Just numStr -> case reads (T.unpack numStr) :: [(Float, String)] of - [(n, "")] -> n / 2 - _ -> 5.0 -- default - Nothing -> 5.0 +type Radius = Float --- | Circle with specified radius and color using ClasshSS -circle' :: DomBuilder t m => Radius -> Color -> m () -circle' rad c = elClass "div" (classhUnsafe [w .~~ twSize' (rad * 2), h .~~ twSize' (rad * 2), bgColor .~~ c, br .~~ R_Full]) $ do - textS (classhUnsafe [text_color .~~ c]) "." +-- | Circle with specified diameter and background/text colors +-- Example: circle (twSize' 10) (only (noTransition (solidColor aceAccent))) (only (color White)) +circle :: DomBuilder t m => TWSizeOrFraction -> WhenTW (WithTransition GradientColor) -> WhenTW ColorWithOpacity -> m () +circle diameter bgCol txtCol = + elClass "div" (classhUnsafe [w .~~ diameter, h .~~ diameter, bgColor .~ bgCol, br .~~ R_Full]) $ do + textS (classhUnsafe [text_color .~ txtCol]) "." + +-- | Circle with specified radius and color (for backwards compatibility) +circle' :: DomBuilder t m => Radius -> WhenTW (WithTransition GradientColor) -> WhenTW ColorWithOpacity -> m () +circle' rad bgCol txtCol = circle (twSize' (rad * 2)) bgCol txtCol -- | Circle with dynamic color -circleDynColor' :: Template t m => Radius -> Dynamic t Color -> m () -circleDynColor' rad dynC = elDynClass "div" (mkBoxStyle <$> dynC) $ do - textDynS (mkTextStyle <$> dynC) "." +circleDynColor :: Template t m => TWSizeOrFraction -> Dynamic t GradientColor -> Dynamic t ColorWithOpacity -> m () +circleDynColor diameter dynBgC dynTxtC = elDynClass "div" (mkBoxStyle <$> dynBgC) $ do + textDynS (mkTextStyle <$> dynTxtC) "." where mkTextStyle c' = (classhUnsafe [text_color .~~ c']) - mkBoxStyle c' = classhUnsafe [ w .~~ twSize' (rad * 2) - , h .~~ twSize' (rad * 2) + mkBoxStyle c' = classhUnsafe [ w .~~ diameter + , h .~~ diameter , bgColor .~~ c' , br .~~ R_Full ] - --- | Legacy dynamic color circle (uses raw HTML) -circleDynColor :: (PostBuild t m, DomBuilder t m) => T.Text -> Dynamic t T.Text -> m () -circleDynColor rad dynColor = - elDynAttr "span" attrs blank - where - attrs = ffor dynColor $ \color -> - ( "style" =: ("border-radius:50%; width: " <> rad <> "; height: " <> rad <> "; display: inline-block;") - <> "class" =: color - ) - -type Radius = Float diff --git a/templates.cabal b/templates.cabal index 37a22a2..6d61534 100644 --- a/templates.cabal +++ b/templates.cabal @@ -1,6 +1,6 @@ name: templates version: 0.1.0.0 -cabal-version: >= 1.8 +cabal-version: >= 1.10 build-type: Simple @@ -21,27 +21,25 @@ library Templates.Types Templates.DomExtras + default-extensions: + ConstraintKinds + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + RankNTypes + RecursiveDo + ScopedTypeVariables + TemplateHaskell - default-extensions: - ConstraintKinds - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - LambdaCase - MultiParamTypeClasses - -- OverloadedStrings - RankNTypes - RecursiveDo - ScopedTypeVariables - TemplateHaskell - - build-depends: base + build-depends: base , ClasshSS , containers , data-default , filepath , lens , reflex-classhss - , reflex-dom - , text \ No newline at end of file + , reflex-dom-core + , text