diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala new file mode 100644 index 0000000..1a05ac9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala @@ -0,0 +1,47 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala new file mode 100644 index 0000000..1a05ac9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala @@ -0,0 +1,47 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala new file mode 100644 index 0000000..03be1ec --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala @@ -0,0 +1,16 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala new file mode 100644 index 0000000..1a05ac9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala @@ -0,0 +1,47 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala new file mode 100644 index 0000000..03be1ec --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala @@ -0,0 +1,16 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala new file mode 100644 index 0000000..72f02aa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala new file mode 100644 index 0000000..1a05ac9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala @@ -0,0 +1,47 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala new file mode 100644 index 0000000..03be1ec --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala @@ -0,0 +1,16 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala new file mode 100644 index 0000000..72f02aa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala b/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala new file mode 100644 index 0000000..701df89 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index cf9aa23..3bbc8c0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -86,7 +86,7 @@ kriterium: Titled[String] ) extends Page( "addProof", - "Nový důkaz", + "Důkaz", Some(DetailKriteria(osobniCislo, parametr, kriterium)) ) @@ -182,7 +182,7 @@ ), root / "osoba" / segment[String] / "parametr" / segment[ String - ] / "kriterium" / segment[String] / "add" / endOfSegments, + ] / "kriterium" / segment[String] / "edit" / endOfSegments, basePath = base ) ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala index ced54cc..b24b82b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -17,4 +17,5 @@ critId: String, page: (UserInfo, Parameter, ParameterCriteria) => Page ) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala deleted file mode 100644 index 78628a8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Avatar.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import CustomAttrs.ariaHidden -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -// TODO: render icon or picture based on img signal -class Avatar($avatarImg: Signal[Option[String]]): - inline def avatarPlaceholder(size: Int): HtmlElement = - div( - cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", - Icons.outline.user(size - 2) - ) - - inline def avatarImage(size: Int): Signal[HtmlElement] = - $avatarImg.split(_ => ())((_, _, $url) => - img( - cls := s"w-$size h-$size rounded-full", - src <-- $url, - alt := "" - ) - ).map(_.getOrElse(avatarPlaceholder(size))) - - inline def avatar(size: Int): HtmlElement = - div( - cls := "relative", - child <-- avatarImage(size), - span( - cls := "absolute inset-0 shadow-inner rounded-full", - ariaHidden := true - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 2ded076..165539a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -2,8 +2,9 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router -import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import fiftyforms.ui.components.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.list.IconText.ViewModel +import fiftyforms.ui.components.Icons import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala deleted file mode 100644 index 9aea064..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala +++ /dev/null @@ -1,77 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -enum ColorWeight(value: String): - inline def toCSS: String = value - // To be used for black and white until - // we support better mechanism via extension methods - case w__ extends ColorWeight("") - case w50 extends ColorWeight("50") - case w100 extends ColorWeight("100") - case w200 extends ColorWeight("200") - case w300 extends ColorWeight("300") - case w400 extends ColorWeight("400") - case w500 extends ColorWeight("500") - case w600 extends ColorWeight("600") - case w700 extends ColorWeight("700") - case w800 extends ColorWeight("800") - case w900 extends ColorWeight("900") - -enum Color(name: String): - import ColorWeight._ - - inline def toCSSNoColorWeight(prefix: String): String = - s"${prefix}-${name}" - inline def toCSSWithColorWeight( - prefix: String, - weight: ColorWeight - ): String = - s"${prefix}-${name}-${weight.toCSS}" - inline def toCSS(prefix: String)(weight: ColorWeight): String = - weight match { - case `w__` => toCSSNoColorWeight(prefix) - case _ => toCSSWithColorWeight(prefix, weight) - } - inline def bg: ColorWeight => String = toCSS("bg")(_) - inline def text: ColorWeight => String = toCSS("text")(_) - inline def decoration: ColorWeight => String = toCSS("decoration")(_) - inline def border: ColorWeight => String = toCSS("border")(_) - inline def outline: ColorWeight => String = toCSS("outline")(_) - inline def divide: ColorWeight => String = toCSS("divide")(_) - inline def ring: ColorWeight => String = toCSS("ring")(_) - inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) - inline def shadow: ColorWeight => String = toCSS("shadow")(_) - inline def accent: ColorWeight => String = toCSS("accent")(_) - - // TODO: change the "stupid" methods to extension methods - // that will keep the invariants in comments lower - case current extends Color("current") - case inherit extends Color("inherit") - // Not present in for all methods - case transp extends Color("transparent") - // Seen in accent, not preset otherwise - case auto extends Color("auto") - // Black and white do not have weight - case black extends Color("black") - case white extends Color("white") - case slate extends Color("slate") - case gray extends Color("gray") - case zinc extends Color("zinc") - case neutral extends Color("neutral") - case stone extends Color("stone") - case red extends Color("red") - case orange extends Color("orange") - case amber extends Color("amber") - case yellow extends Color("yellow") - case lime extends Color("lime") - case green extends Color("green") - case emerald extends Color("emerald") - case teal extends Color("teal") - case cyan extends Color("cyan") - case sky extends Color("sky") - case blue extends Color("blue") - case indigo extends Color("indigo") - case violet extends Color("violet") - case purple extends Color("purple") - case fuchsia extends Color("fuchsia") - case pink extends Color("pink") - case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala deleted file mode 100644 index 1da4460..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/CustomAttrs.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object CustomAttrs { - // Made a pull request to add aria-current to scala-dom-types, remove after - val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) - val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - - val datetime = customHtmlAttr("datetime", StringAsIsCodec) - - object svg { - import com.raquo.laminar.api.L.svg.{*, given} - val ariaHidden = - customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) - } -} diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala deleted file mode 100644 index eadb044..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala +++ /dev/null @@ -1,28 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -object Display: - - enum Breakpoint: - case sm, md, lg, xl, `2xl` - - enum DisplayClass: - case block, `inline-block`, `inline`, flex, `inline-flex`, table, - `inline-table`, `table-caption` - - object ShowUpFrom: - inline def apply( - br: Breakpoint, - dc: DisplayClass = DisplayClass.block - ): HtmlElement = - div( - cls := "hidden", - cls := s"${br}:${dc}" - ) - - object HideUpTo: - inline def apply(br: Breakpoint): HtmlElement = - div( - cls := s"${br}:hidden" - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala deleted file mode 100644 index f655da4..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Icons.scala +++ /dev/null @@ -1,254 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -import com.raquo.domtypes.generic.defs.attrs.AriaAttrs -import com.raquo.laminar.api.L.svg.{*, given} -import com.raquo.laminar.builders.SvgBuilders -import com.raquo.laminar.keys.ReactiveSvgAttr - -// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site -object Icons: - object aria: - inline def hidden = CustomAttrs.svg.ariaHidden - - object outline: - val defaultSize: Int = 6 - - inline def bell(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" - ) - ) - - inline def `check-circle`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" - ) - ) - - inline def `document-add`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" - ) - ) - - inline def `external-link`(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" - ) - ) - - inline def menu(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M4 6h16M4 12h16M4 18h16" - ) - ) - - inline def x(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - fill := "none", - viewBox := "0 0 24 24", - stroke := "currentColor", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M6 18L18 6M6 6l12 12" - ) - ) - - inline def user(size: Int = defaultSize) = - svg( - cls := s"w-${size} h-${size}", - fill := "none", - stroke := "currentColor", - viewBox := "0 0 24 24", - xmlns := "http://www.w3.org/2000/svg", - aria.hidden := true, - path( - strokeLineCap := "round", - strokeLineJoin := "round", - strokeWidth := "2", - d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" - ) - ) - - end outline - - object solid: - val defaultSize: Int = 5 - - inline def users(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" - ) - ) - - inline def `location-marker`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", - clipRule := "evenodd" - ) - ) - - inline def calendar(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", - clipRule := "evenodd" - ) - ) - - inline def `chevron-right`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", - clipRule := "evenodd" - ) - ) - - inline def search(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", - clipRule := "evenodd" - ) - ) - - inline def filter(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", - clipRule := "evenodd" - ) - ) - - inline def `arrow-narrow-left`(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", - clipRule := "evenodd" - ) - ) - - inline def home(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size}", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" - ) - ) - - inline def paperclip(size: Int = defaultSize) = - svg( - cls := s"h-${size} w-${size} text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - aria.hidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - - end solid -end Icons diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala deleted file mode 100644 index 6459b73..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent - -object LinkSupport: - - extension [El <: org.scalajs.dom.EventTarget]( - ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] - ) - def noKeyMod = - ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala deleted file mode 100644 index 7c7bb07..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} - -// TODO: proper loader -def Loading = - div( - cls := "bg-gray-50 overflow-hidden rounded-lg", - div( - cls := "px-4 py-5 sm:p-6", - "Loading..." - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index b856816..fb0002d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,7 +1,9 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import CustomAttrs.ariaCurrent +import fiftyforms.ui.components.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Avatar import cz.e_bs.cmi.mdr.pdb.UserInfo import io.laminext.syntax.core.* diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 7147460..6154cf0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action +import fiftyforms.ui.components.Loading import io.laminext.syntax.core.* object PageLayout: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala index 0cb8fe5..167879b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import LinkSupport.* +import fiftyforms.ui.components.LinkSupport.* import cz.e_bs.cmi.mdr.pdb.app.Page import cz.e_bs.cmi.mdr.pdb.app.Action import com.raquo.waypoint.Router diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala deleted file mode 100644 index 930c815..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -case class File(url: String, name: String) - -object File: - extension (m: File) - def toHtml: HtmlElement = - li( - cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), - div( - cls("w-0 flex-1 flex items-center"), - Icons.solid - .paperclip() - .amend(svg.cls := "flex-shrink-0 text-gray-400"), - span(cls("ml-2 flex-1 w-0 truncate"), m.name) - ), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala deleted file mode 100644 index 12bbac9..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} - -def FileList(files: List[File]): HtmlElement = - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - files.map(_.toHtml) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala deleted file mode 100644 index b4e506a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} - -object FilePicker: - import cz.e_bs.cmi.mdr.pdb.app.components.files - - val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] - val selectorOpen = Var[Boolean](false) - - def apply(chosenFiles: Var[List[File]]): HtmlElement = - // This sequence tricks browser into displaying modal content centered - // Inspired by modal in headless ui playground - // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 - inline def browserCenteringModalTrick: Modifier[HtmlElement] = - Seq[Modifier[HtmlElement]]( - span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), - "​" // Zero width space - ) - - inline def overlay: Modifier[HtmlElement] = - // Page overlay - /* TODO: transition - enter="ease-out duration-300" - enterFrom="opacity-0" - enterTo="opacity-100" - leave="ease-in duration-200" - leaveFrom="opacity-100" - leaveTo="opacity-0" - */ - div( - div( - onClick.mapTo(false) --> selectorOpen.writer, - cls("fixed inset-0 transition-opacity"), - div(cls("absolute inset-0 bg-gray-500 opacity-75")) - ) - ) - - inline def modalSelector: HtmlElement = - div( - cls("fixed inset-0 z-10 overflow-y-auto"), - div( - cls("text-center sm:block sm:p-0"), - overlay, - browserCenteringModalTrick, - child <-- chosenFiles.signal.map( - FileSelector( - _, - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") - ) - ) - )(fsaObserver) - ) - ) - ) - - div( - fsaStream.collect { case FileSelector.SelectionUpdated(files) => - files.to(List) - } --> chosenFiles.writer, - fsaStream.mapTo(false) --> selectorOpen.writer, - child.maybe <-- selectorOpen.signal.map(isOpen => - if isOpen then Some(modalSelector) else None - ), - div( - cls("flex flex-col space-y-5"), - child.maybe <-- chosenFiles.signal.map(files => - if files.isEmpty then None else Some(FileList(files)) - ), - button( - tpe := "button", - cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zvolit soubory", - onClick.mapTo(true) --> selectorOpen.writer - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala deleted file mode 100644 index a14d16d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import io.laminext.syntax.core.{*, given} -import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec - -object FileSelector: - sealed trait Action - case class SelectionUpdated(files: Set[File]) extends Action - case object SelectionCancelled extends Action - def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( - selectionDone: Observer[Action] - ): HtmlElement = - val selectedFiles = Var[Set[File]](initialFiles.to(Set)) - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), - div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" - ) - ) - ), - FileTable(availableFiles, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit", - composeEvents(onClick)( - _.sample(selectedFiles) - .map(SelectionUpdated(_)) - ) --> selectionDone - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit", - onClick.mapTo(SelectionCancelled) --> selectionDone - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala deleted file mode 100644 index 0fcc458..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.files - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), - div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), - div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala deleted file mode 100644 index 865ffbc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/ComboBox.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs - -case class ComboBox( - id: String, - options: Signal[List[ComboBox.Option]], - valueUpdates: Observer[List[String]] -) - -object ComboBox: - - extension (m: ComboBox) - def toHtml: HtmlElement = - val isOpen = Var(false) - div( - cls := "relative mt-1", - input( - idAttr := m.id, - tpe := "text", - cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", - role := "combobox", - aria.controls := "options", - aria.expanded := false, - onClick.mapTo(true) --> isOpen.writer - ), - button( - tpe := "button", - cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { - import svg.* - svg( - cls := "h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", - clipRule := "evenodd" - ) - ) - } - ), - ul( - cls <-- isOpen.signal.switch("", "hidden"), - cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", - idAttr := "options", - role := "listbox", - children <-- m.options.map(_.map(_.toHtml)) - ) - ) - - case class Option(value: String, active: Boolean) - - object Option: - extension (m: Option) - def toHtml: HtmlElement = - li( - cls := "relative cursor-default select-none py-2 pl-8 pr-4", - cls := (if m.active then "text-white bg-indigo-600" - else "text-gray-900"), - idAttr := "option-0", - role := "option", - tabIndex := -1, - span( - cls := "block truncate", - m.value - ), - if m.active then - span( - cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", - cls := (if m.active then "text-white" else "text-indigo-600"), { - import svg.* - svg( - cls := "h-5 w-5", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - CustomAttrs.svg.ariaHidden := true, - path( - fillRule := "evenodd", - d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", - clipRule := "evenodd" - ) - ) - } - ) - else emptyNode - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala deleted file mode 100644 index b76f093..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/Form.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object Form: - val Body = FormBody - val Section = FormSection - val Row = FormRow - - def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = - form( - cls := "space-y-8 divide-y divide-gray-200", - body, - buttons - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala deleted file mode 100644 index b489752..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormBody.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormBody: - def apply(sections: HtmlElement*): HtmlElement = - div( - cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", - sections - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala deleted file mode 100644 index f7cff1b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormFields.scala +++ /dev/null @@ -1,14 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement -import org.scalajs.dom - -object FormFields: - def apply( - mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* - ): HtmlElement = - div( - cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala deleted file mode 100644 index 235e320..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormHeader.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -object FormHeader: - case class ViewModel(header: String, description: String) - def apply(m: ViewModel): HtmlElement = - div( - h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), - p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala deleted file mode 100644 index 4007853..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormRow.scala +++ /dev/null @@ -1,22 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} - -case class FormRow(id: String, label: String, content: Modifier[Div]) - -object FormRow: - - extension (m: FormRow) - def toHtml: HtmlElement = - div( - cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", - label( - forId := m.id, - cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", - m.label - ), - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - m.content - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala deleted file mode 100644 index e09e76d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/form/FormSection.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.form - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.laminar.nodes.ReactiveHtmlElement - -object FormSection: - def apply( - header: HtmlElement, - rows: HtmlElement* - ): HtmlElement = - div( - cls := "space-y-6 sm:space-y-5", - header, - rows - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala deleted file mode 100644 index 1d7d448..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/BaseList.scala +++ /dev/null @@ -1,142 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import com.raquo.laminar.builders.HtmlTag -import org.scalajs.dom -import cz.e_bs.cmi.mdr.pdb.app.components.Icons - -trait ListItem: - def key: String - def title: Modifier[HtmlElement] - def topRight: Modifier[HtmlElement] - def bottomLeft: Modifier[HtmlElement] - def bottomRight: Modifier[HtmlElement] - -trait ListRenderable[Item]: - extension (x: Item) def asItem: ListItem - -trait Navigable[Item]: - extension (x: Item) def navigate: Modifier[HtmlElement] - -object BaseList: - enum Color: - case Green, Yellow, Red - - case class IconText(text: HtmlElement, icon: SvgElement) - case class Tag(text: String, color: Color) - case class Row( - id: String, - title: String, - tag: Tag, - leftProps: List[IconText], - rightProp: IconText - ) - - trait AsRow[Data]: - extension (d: Data) def asRow: Row - - class RowListItem(d: Row) extends ListItem: - - def key: String = d.id - - def title: Modifier[HtmlElement] = d.title - - def topRight: Modifier[HtmlElement] = - inline def colorClass(color: Color): (String, Boolean) = - val c = color.toString.toLowerCase - s"bg-$c-100 text-$c-800" -> (d.tag.color == color) - - inline def colors = Map(Color.values.map(colorClass(_)): _*) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls := colors, - d.tag.text - ) - - def bottomLeft: Modifier[HtmlElement] = - div( - cls := "sm:flex", - d.leftProps.zipWithIndex.map { case (i, idx) => - p( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), - cls := "flex items-center text-sm text-gray-500", - i.icon, - i.text - ) - } - ) - - def bottomRight: Modifier[HtmlElement] = - div( - cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", - d.rightProp.icon, - d.rightProp.text - ) - - object Row: - given asRowRenderable[T: AsRow]: ListRenderable[T] with - extension (d: T) def asItem = new RowListItem(d.asRow) - - end Row - -class BaseList[RowData: ListRenderable]: - - protected def containerElement: HtmlTag[dom.html.Element] = div - protected def containerMods(rowData: RowData): Modifier[HtmlElement] = - emptyMod - protected def farRight: Modifier[HtmlElement] = emptyMod - - def render($data: Signal[List[RowData]]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) - ) - - private def row(d: RowData): HtmlElement = - val data = d.asItem - li( - containerElement( - containerMods(d), - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - data.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - data.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - data.bottomLeft, - data.bottomRight - ) - ), - farRight - ) - ) - ) - -trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) - extends BaseList[RowData]: - - override protected def containerElement: HtmlTag[dom.html.Element] = a - override protected def containerMods( - rowData: RowData - ): Modifier[HtmlElement] = - rowData.navigate - override protected def farRight: Modifier[HtmlElement] = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala deleted file mode 100644 index 13f747d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom -import com.raquo.laminar.builders.HtmlTag - -object IconText: - case class ViewModel(text: HtmlElement, icon: SvgElement) - def render($m: Signal[ViewModel]): HtmlElement = render($m, div) - def render( - $m: Signal[ViewModel], - container: HtmlTag[dom.html.Element] - ): HtmlElement = - container( - cls := "flex items-center text-sm text-gray-500", - child <-- $m.map(_.icon), - child <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala deleted file mode 100644 index 097fbb7..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ /dev/null @@ -1,46 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} -import org.scalajs.dom - -object ListRow: - case class ViewModel( - title: String, - topRight: Modifier[HtmlElement], - bottomLeft: Modifier[HtmlElement], - bottomRight: Modifier[HtmlElement], - farRight: Modifier[HtmlElement], - containerElement: HtmlElement = div() - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - li( - child <-- $m.map(m => - m.containerElement.amend( - cls := "block hover:bg-gray-50", - div( - cls := "px-4 py-4 sm:px-6 items-center flex", - div( - cls := "min-w-0 flex-1 pr-4", - div( - cls := "flex items-center justify-between", - p( - cls := "text-sm font-medium text-indigo-600 truncate", - m.title - ), - div( - cls := "ml-2 flex-shrink-0 flex", - m.topRight - ) - ), - div( - cls := "mt-2 sm:flex sm:justify-between", - m.bottomLeft, - m.bottomRight - ) - ), - m.farRight - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala deleted file mode 100644 index 9c6c5eb..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala +++ /dev/null @@ -1,16 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object PropList: - type ViewModel = List[HtmlElement] - def render($m: Signal[ViewModel]): HtmlElement = - div( - cls := "sm:flex", - children <-- $m.map(_.zipWithIndex.map { case (i, idx) => - i.amend( - cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) - ) - }) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala deleted file mode 100644 index f45e8cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowNext: - def render: HtmlElement = - div( - cls := "flex-shrink-0", - Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala deleted file mode 100644 index cc85a37..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components -package list - -import com.raquo.laminar.api.L.{*, given} - -object RowTag: - case class ViewModel(text: String, color: Color) - def render($m: Signal[ViewModel]): HtmlElement = - inline def colorClass(color: Color): Seq[String] = - import ColorWeight._ - List(color.bg(w100), color.text(w800)) - - p( - cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", - cls <-- $m.map(t => colorClass(t.color)), - child.text <-- $m.map(_.text) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala deleted file mode 100644 index e80fba6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ /dev/null @@ -1,15 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components.list - -import com.raquo.laminar.api.L.{*, given} - -class StackedList[Item]: - type ViewModel = List[Item] - def apply( - $m: Signal[ViewModel], - keyF: Item => String - )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = - ul( - role := "list", - cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 0d6e254..aa6adc0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -10,7 +10,7 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailPageConnector { trait AppState { diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index ba685e2..596d59f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -5,12 +5,12 @@ import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.app.components.Color import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.Color extension (o: UserInfo) def toDetailOsoby: DetailOsoby.ViewModel = diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala index 0ba1acd..9c31944 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -6,6 +6,7 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.components.File object UpravDukaz: @@ -15,6 +16,7 @@ trait State { def details: EventStream[UserInfo] def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] def actionBus: Observer[Action] } @@ -51,8 +53,12 @@ $merged.split(_ => ())((_, s, $s) => PageComponent( $s.map(buildModel), - state.actionBus.contramap { case UpravDukazForm.Cancel => - NavigateTo(Page.DetailKriteria.fromProduct(s)) + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) } ) ), @@ -82,7 +88,8 @@ def apply( $m: Signal[ViewModel], - events: Observer[UpravDukazForm.Action] + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] ): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", @@ -94,7 +101,7 @@ ), div( DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(events) + UpravDukazForm(availableFilesStream)(events) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala index 4cb3c0c..8c41ef8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -1,10 +1,10 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.Icons import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailKriteria: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala index 67f6ca2..1b56d2e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -4,7 +4,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object DetailOsoby: @@ -36,7 +36,7 @@ ) def render($m: Signal[ViewModel]): HtmlElement = - import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + import fiftyforms.ui.components.CustomAttrs.datetime p( cls := "text-sm font-medium text-gray-500", child.text <-- $m.map(_.druh), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala index 35ad70f..9542bca 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color +import fiftyforms.ui.components.Color object DetailParametru: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala index 64ae492..4683ece 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import java.time.LocalDate -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs +import fiftyforms.ui.components.CustomAttrs object DukazKriteria: case class Osoba(osobniCislo: String, jmeno: String) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala index 977fc9d..5692977 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object DukazyKriteria: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index 6eb385c..5530c58 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -1,11 +1,11 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.list.StackedList -import cz.e_bs.cmi.mdr.pdb.app.components.list.ListRow -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowTag -import cz.e_bs.cmi.mdr.pdb.app.components.list.RowNext +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.list.StackedList +import fiftyforms.ui.components.list.ListRow +import fiftyforms.ui.components.list.RowTag +import fiftyforms.ui.components.list.RowNext import java.time.LocalDate object SeznamKriterii: diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index f9e8390..f9bd482 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.list.{ +import fiftyforms.ui.components.list.{ StackedList, ListRow, RowTag, @@ -9,8 +9,8 @@ IconText, RowNext } -import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* +import fiftyforms.ui.components.Color +import fiftyforms.ui.components.LinkSupport.* object SeznamParametru: type ViewModel = List[DetailParametru.ViewModel] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 5510d6c..dd6d8b1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -2,20 +2,24 @@ import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.* -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs -import cz.e_bs.cmi.mdr.pdb.app.components.form.* +import fiftyforms.ui.components.CustomAttrs +import fiftyforms.ui.components.form.* import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement -import cz.e_bs.cmi.mdr.pdb.app.components.files -import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import fiftyforms.services.files.components.FilePicker import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import cz.e_bs.cmi.mdr.pdb.app.components.files.File +import fiftyforms.services.files.components.File object UpravDukazForm: - sealed trait Action - case object Cancel extends Action - def apply(onEvent: Observer[Action]): HtmlElement = + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] val files = Var[List[File]](Nil) def submitButtons: HtmlElement = div( @@ -26,7 +30,7 @@ tpe := "button", cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", "Zrušit", - onClick.mapTo(Cancel) --> onEvent + onClick.mapTo(Cancelled) --> updates ), button( tpe := "submit", @@ -38,6 +42,12 @@ ) div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( Form.Body( @@ -52,7 +62,7 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker(files) + FilePicker(files.signal, availableFilesStream)(filesObserver) .amend(idAttr := "dokumenty", cls("max-w-lg")) ).toHtml, FormRow( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala index 6e0d757..3514e00 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -1,7 +1,7 @@ package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import fiftyforms.ui.components.Icons object SearchForm: sealed trait Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala index 7cbdd1f..dc91914 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.airstream.core.Signal -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import fiftyforms.ui.components.Avatar object UserRow: case class ViewModel( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala index 98b5c58..47a6df0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala @@ -14,6 +14,7 @@ import cz.e_bs.cmi.mdr.pdb.ParameterCriteria import cz.e_bs.cmi.mdr.pdb.UserFunction import cz.e_bs.cmi.mdr.pdb.UserContract +import fiftyforms.services.files.components.File trait AppState extends connectors.DetailPageConnector.AppState @@ -42,6 +43,7 @@ private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val mockData: List[UserInfo] = mockUsers @@ -94,6 +96,12 @@ pushParameters(mockParameters) router.replaceState(page(o, p, c)) case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) } override def users: EventStream[List[UserInfo]] = @@ -105,5 +113,8 @@ override def parameters: EventStream[List[Parameter]] = parametersStream.debugWithName("parameters") + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + override def actionBus: Observer[Action] = actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 442e737..247d3d5 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,20 @@ .crossType(CrossType.Pure) .in(file("core")) +lazy val ui = (project in file("ui")) + .enablePlugins(ScalaJSPlugin) + .settings( + IWDeps.useZIO(Test), + IWDeps.laminar, + IWDeps.zioJson, + IWDeps.waypoint, + IWDeps.urlDsl, + IWDeps.laminextCore, + IWDeps.laminextUI, + IWDeps.laminextTailwind, + IWDeps.laminextValidationCore + ) + lazy val app = (project in file("app")) .enablePlugins(ScalaJSPlugin, VitePlugin, MockDataExport) .settings( @@ -33,7 +47,7 @@ scalaJSLinkerConfig ~= { _.withSourceMap(false) }, scalaJSUseMainModuleInitializer := true ) - .dependsOn(core.js) + .dependsOn(core.js, ui) lazy val server = (project in file("server")) .enablePlugins(DockerPlugin, JavaServerAppPackaging) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/File.scala b/ui/src/main/scala/fiftyforms/services/files/components/File.scala new file mode 100644 index 0000000..1b87ad1 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/File.scala @@ -0,0 +1,25 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons + +case class File(url: String, name: String) + +object File: + extension (m: File) + def toHtml: HtmlElement = + li( + cls("pl-3 pr-4 py-3 flex items-center justify-between text-sm"), + div( + cls("w-0 flex-1 flex items-center"), + Icons.solid + .paperclip() + .amend(svg.cls := "flex-shrink-0 text-gray-400"), + span(cls("ml-2 flex-1 w-0 truncate"), m.name) + ), + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala new file mode 100644 index 0000000..b69a6c0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileList.scala @@ -0,0 +1,12 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.Icons +import io.laminext.syntax.core.{*, given} + +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala new file mode 100644 index 0000000..3e2eff9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FilePicker.scala @@ -0,0 +1,80 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} + +object FilePicker: + sealed trait Event + sealed trait DoneEvent extends Event + case class SelectionUpdated(files: Set[File]) extends DoneEvent + case object SelectionCancelled extends DoneEvent + case object AvailableFilesRequested extends Event + + def apply( + currentFiles: Signal[List[File]], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val (updatesStream, updatesObserver) = EventStream.withObserver[Event] + val selectorOpen = Var[Boolean](false) + + // This sequence tricks browser into displaying modal content centered + // Inspired by modal in headless ui playground + // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 + inline def browserCenteringModalTrick: Modifier[HtmlElement] = + Seq[Modifier[HtmlElement]]( + span(cls("hidden sm:inline-block sm:h-screen sm:align-middle")), + "​" // Zero width space + ) + + inline def overlay: Modifier[HtmlElement] = + // Page overlay + /* TODO: transition + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + */ + div( + div( + onClick.mapTo(false) --> selectorOpen.writer, + cls("fixed inset-0 transition-opacity"), + div(cls("absolute inset-0 bg-gray-500 opacity-75")) + ) + ) + + inline def modalSelector: HtmlElement = + div( + cls("fixed inset-0 z-10 overflow-y-auto"), + div( + cls("text-center sm:block sm:p-0"), + overlay, + browserCenteringModalTrick, + child <-- currentFiles.map( + FileSelector(_, availableFilesStream)(updatesObserver) + ) + ) + ) + + div( + updatesStream --> selectionUpdates, + updatesStream.collect { case _: DoneEvent => + false + } --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None + ), + div( + cls("flex flex-col space-y-5"), + child.maybe <-- currentFiles.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala new file mode 100644 index 0000000..b7cb12c --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileSelector.scala @@ -0,0 +1,73 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons +import fiftyforms.ui.components.Loading +import io.laminext.syntax.core.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object FileSelector: + import FilePicker._ + + def apply( + initialFiles: List[File], + availableFilesStream: EventStream[List[File]] + )(selectionUpdates: Observer[Event]): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) + val availableFiles = availableFilesStream.startWithNone + // Request the files to display + selectionUpdates.onNext(AvailableFilesRequested) + div( + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), + div( + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), + div( + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) + ) + ), + child <-- availableFiles + .split(_ => ())((_, _, af) => FileTable(af, selectedFiles)) + .map(_.getOrElse(Loading)) + ), + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionUpdates + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionUpdates + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala new file mode 100644 index 0000000..d71395a --- /dev/null +++ b/ui/src/main/scala/fiftyforms/services/files/components/FileTable.scala @@ -0,0 +1,90 @@ +package fiftyforms.services.files.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import fiftyforms.ui.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala new file mode 100644 index 0000000..42473bf --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Avatar.scala @@ -0,0 +1,32 @@ +package fiftyforms.ui.components + +import CustomAttrs.ariaHidden +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +// TODO: render icon or picture based on img signal +class Avatar($avatarImg: Signal[Option[String]]): + inline def avatarPlaceholder(size: Int): HtmlElement = + div( + cls := s"rounded-full text-indigo-200 bg-indigo-500 h-${size} w-${size} flex items-center justify-center", + Icons.outline.user(size - 2) + ) + + inline def avatarImage(size: Int): Signal[HtmlElement] = + $avatarImg.split(_ => ())((_, _, $url) => + img( + cls := s"w-$size h-$size rounded-full", + src <-- $url, + alt := "" + ) + ).map(_.getOrElse(avatarPlaceholder(size))) + + inline def avatar(size: Int): HtmlElement = + div( + cls := "relative", + child <-- avatarImage(size), + span( + cls := "absolute inset-0 shadow-inner rounded-full", + ariaHidden := true + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Color.scala b/ui/src/main/scala/fiftyforms/ui/components/Color.scala new file mode 100644 index 0000000..5ef6cb4 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Color.scala @@ -0,0 +1,77 @@ +package fiftyforms.ui.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala new file mode 100644 index 0000000..5d1144d --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/CustomAttrs.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec + +object CustomAttrs { + // Made a pull request to add aria-current to scala-dom-types, remove after + val ariaCurrent = customHtmlAttr("aria-current", StringAsIsCodec) + val ariaHidden = customHtmlAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + + val datetime = customHtmlAttr("datetime", StringAsIsCodec) + + object svg { + import com.raquo.laminar.api.L.svg.{*, given} + val ariaHidden = + customSvgAttr("aria-hidden", BooleanAsTrueFalseStringCodec) + } +} diff --git a/ui/src/main/scala/fiftyforms/ui/components/Display.scala b/ui/src/main/scala/fiftyforms/ui/components/Display.scala new file mode 100644 index 0000000..a6635f0 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Display.scala @@ -0,0 +1,28 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Icons.scala b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala new file mode 100644 index 0000000..8df4acb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Icons.scala @@ -0,0 +1,254 @@ +package fiftyforms.ui.components + +import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec +import com.raquo.domtypes.generic.defs.attrs.AriaAttrs +import com.raquo.laminar.api.L.svg.{*, given} +import com.raquo.laminar.builders.SvgBuilders +import com.raquo.laminar.keys.ReactiveSvgAttr + +// TODO: fix sizes, colors, hover and stuff, normalize and amend on call site +object Icons: + object aria: + inline def hidden = CustomAttrs.svg.ariaHidden + + object outline: + val defaultSize: Int = 6 + + inline def bell(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + ) + ) + + inline def `check-circle`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" + ) + ) + + inline def `document-add`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" + ) + ) + + inline def `external-link`(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" + ) + ) + + inline def menu(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M4 6h16M4 12h16M4 18h16" + ) + ) + + inline def x(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + fill := "none", + viewBox := "0 0 24 24", + stroke := "currentColor", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M6 18L18 6M6 6l12 12" + ) + ) + + inline def user(size: Int = defaultSize) = + svg( + cls := s"w-${size} h-${size}", + fill := "none", + stroke := "currentColor", + viewBox := "0 0 24 24", + xmlns := "http://www.w3.org/2000/svg", + aria.hidden := true, + path( + strokeLineCap := "round", + strokeLineJoin := "round", + strokeWidth := "2", + d := "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" + ) + ) + + end outline + + object solid: + val defaultSize: Int = 5 + + inline def users(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" + ) + ) + + inline def `location-marker`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z", + clipRule := "evenodd" + ) + ) + + inline def calendar(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z", + clipRule := "evenodd" + ) + ) + + inline def `chevron-right`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", + clipRule := "evenodd" + ) + ) + + inline def search(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", + clipRule := "evenodd" + ) + ) + + inline def filter(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z", + clipRule := "evenodd" + ) + ) + + inline def `arrow-narrow-left`(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z", + clipRule := "evenodd" + ) + ) + + inline def home(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size}", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + d := "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" + ) + ) + + inline def paperclip(size: Int = defaultSize) = + svg( + cls := s"h-${size} w-${size} text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + aria.hidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + + end solid +end Icons diff --git a/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala new file mode 100644 index 0000000..7c4bbfa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/ui/src/main/scala/fiftyforms/ui/components/Loader.scala b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala new file mode 100644 index 0000000..8c6dbe5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/Loader.scala @@ -0,0 +1,13 @@ +package fiftyforms.ui.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala new file mode 100644 index 0000000..16d6dbb --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/ComboBox.scala @@ -0,0 +1,92 @@ +package fiftyforms.ui.components +package form + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* + +case class ComboBox( + id: String, + options: Signal[List[ComboBox.Option]], + valueUpdates: Observer[List[String]] +) + +object ComboBox: + + extension (m: ComboBox) + def toHtml: HtmlElement = + val isOpen = Var(false) + div( + cls := "relative mt-1", + input( + idAttr := m.id, + tpe := "text", + cls := "w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm", + role := "combobox", + aria.controls := "options", + aria.expanded := false, + onClick.mapTo(true) --> isOpen.writer + ), + button( + tpe := "button", + cls := "absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none", { + import svg.* + svg( + cls := "h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z", + clipRule := "evenodd" + ) + ) + } + ), + ul( + cls <-- isOpen.signal.switch("", "hidden"), + cls := "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm", + idAttr := "options", + role := "listbox", + children <-- m.options.map(_.map(_.toHtml)) + ) + ) + + case class Option(value: String, active: Boolean) + + object Option: + extension (m: Option) + def toHtml: HtmlElement = + li( + cls := "relative cursor-default select-none py-2 pl-8 pr-4", + cls := (if m.active then "text-white bg-indigo-600" + else "text-gray-900"), + idAttr := "option-0", + role := "option", + tabIndex := -1, + span( + cls := "block truncate", + m.value + ), + if m.active then + span( + cls := "absolute inset-y-0 left-0 flex items-center pl-1.5", + cls := (if m.active then "text-white" else "text-indigo-600"), { + import svg.* + svg( + cls := "h-5 w-5", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + CustomAttrs.svg.ariaHidden := true, + path( + fillRule := "evenodd", + d := "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", + clipRule := "evenodd" + ) + ) + } + ) + else emptyNode + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala new file mode 100644 index 0000000..6188660 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/Form.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object Form: + val Body = FormBody + val Section = FormSection + val Row = FormRow + + def apply(body: HtmlElement, buttons: HtmlElement): HtmlElement = + form( + cls := "space-y-8 divide-y divide-gray-200", + body, + buttons + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala new file mode 100644 index 0000000..02c89b8 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormBody.scala @@ -0,0 +1,10 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormBody: + def apply(sections: HtmlElement*): HtmlElement = + div( + cls := "space-y-8 divide-y divide-gray-200 sm:space-y-5", + sections + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala new file mode 100644 index 0000000..39d1f1f --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormFields.scala @@ -0,0 +1,14 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement +import org.scalajs.dom + +object FormFields: + def apply( + mods: Modifier[ReactiveHtmlElement[dom.HTMLElement]]* + ): HtmlElement = + div( + cls := "mt-6 sm:mt-5 space-y-6 sm:space-y-5", + mods + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala new file mode 100644 index 0000000..e1c1d5b --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormHeader.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +object FormHeader: + case class ViewModel(header: String, description: String) + def apply(m: ViewModel): HtmlElement = + div( + h3(cls := "text-lg leading-6 font-medium text-gray-900", m.header), + p(cls := "mt-1 max-w-2xl text-sm text-gray-500", m.description) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala new file mode 100644 index 0000000..cbd41c5 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormRow.scala @@ -0,0 +1,22 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} + +case class FormRow(id: String, label: String, content: Modifier[Div]) + +object FormRow: + + extension (m: FormRow) + def toHtml: HtmlElement = + div( + cls := "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5", + label( + forId := m.id, + cls := "block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2", + m.label + ), + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + m.content + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala new file mode 100644 index 0000000..53ddb34 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/form/FormSection.scala @@ -0,0 +1,15 @@ +package fiftyforms.ui.components.form + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.laminar.nodes.ReactiveHtmlElement + +object FormSection: + def apply( + header: HtmlElement, + rows: HtmlElement* + ): HtmlElement = + div( + cls := "space-y-6 sm:space-y-5", + header, + rows + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala new file mode 100644 index 0000000..6df8e28 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/BaseList.scala @@ -0,0 +1,142 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import com.raquo.laminar.builders.HtmlTag +import org.scalajs.dom + +trait ListItem: + def key: String + def title: Modifier[HtmlElement] + def topRight: Modifier[HtmlElement] + def bottomLeft: Modifier[HtmlElement] + def bottomRight: Modifier[HtmlElement] + +trait ListRenderable[Item]: + extension (x: Item) def asItem: ListItem + +trait Navigable[Item]: + extension (x: Item) def navigate: Modifier[HtmlElement] + +object BaseList: + enum Color: + case Green, Yellow, Red + + case class IconText(text: HtmlElement, icon: SvgElement) + case class Tag(text: String, color: Color) + case class Row( + id: String, + title: String, + tag: Tag, + leftProps: List[IconText], + rightProp: IconText + ) + + trait AsRow[Data]: + extension (d: Data) def asRow: Row + + class RowListItem(d: Row) extends ListItem: + + def key: String = d.id + + def title: Modifier[HtmlElement] = d.title + + def topRight: Modifier[HtmlElement] = + inline def colorClass(color: Color): (String, Boolean) = + val c = color.toString.toLowerCase + s"bg-$c-100 text-$c-800" -> (d.tag.color == color) + + inline def colors = Map(Color.values.map(colorClass(_)): _*) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls := colors, + d.tag.text + ) + + def bottomLeft: Modifier[HtmlElement] = + div( + cls := "sm:flex", + d.leftProps.zipWithIndex.map { case (i, idx) => + p( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)), + cls := "flex items-center text-sm text-gray-500", + i.icon, + i.text + ) + } + ) + + def bottomRight: Modifier[HtmlElement] = + div( + cls := "mt-2 flex items-center text-sm text-gray-500 sm:mt-0", + d.rightProp.icon, + d.rightProp.text + ) + + object Row: + given asRowRenderable[T: AsRow]: ListRenderable[T] with + extension (d: T) def asItem = new RowListItem(d.asRow) + + end Row + +class BaseList[RowData: ListRenderable]: + + protected def containerElement: HtmlTag[dom.html.Element] = div + protected def containerMods(rowData: RowData): Modifier[HtmlElement] = + emptyMod + protected def farRight: Modifier[HtmlElement] = emptyMod + + def render($data: Signal[List[RowData]]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $data.split(_.asItem.key)((_, d, _) => row(d)) + ) + + private def row(d: RowData): HtmlElement = + val data = d.asItem + li( + containerElement( + containerMods(d), + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + data.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + data.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + data.bottomLeft, + data.bottomRight + ) + ), + farRight + ) + ) + ) + +trait NavigableList[RowData: Navigable, Page](using router: Router[Page]) + extends BaseList[RowData]: + + override protected def containerElement: HtmlTag[dom.html.Element] = a + override protected def containerMods( + rowData: RowData + ): Modifier[HtmlElement] = + rowData.navigate + override protected def farRight: Modifier[HtmlElement] = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala new file mode 100644 index 0000000..9cd6dc9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/IconText.scala @@ -0,0 +1,19 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala new file mode 100644 index 0000000..1a05ac9 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/ListRow.scala @@ -0,0 +1,47 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala new file mode 100644 index 0000000..03be1ec --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/PropList.scala @@ -0,0 +1,16 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala new file mode 100644 index 0000000..72f02aa --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right`().amend(svg.cls := "text-gray-400") + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala b/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala new file mode 100644 index 0000000..701df89 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) diff --git a/ui/src/main/scala/fiftyforms/ui/components/list/StackedList.scala b/ui/src/main/scala/fiftyforms/ui/components/list/StackedList.scala new file mode 100644 index 0000000..aa13ac3 --- /dev/null +++ b/ui/src/main/scala/fiftyforms/ui/components/list/StackedList.scala @@ -0,0 +1,16 @@ +package fiftyforms.ui.components +package list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def apply( + $m: Signal[ViewModel], + keyF: Item => String + )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = + ul( + role := "list", + cls := "divide-y divide-gray-200", + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) + )